Java Language
Java-fallgropar - Nulls och NullPointerException
Sök…
Anmärkningar
Värdet null
är standardvärdet för ett oinitialiserat värde för ett fält vars typ är en referenstyp.
NullPointerException
(eller NPE) är undantaget som kastas när du försöker utföra en olämplig operation på null
Sådana operationer inkluderar:
- anropa en instansmetod på ett
null
, - åtkomst till ett fält för ett
null
, - försöker indexera ett
null
eller få åtkomst till dess längd, - använder en
null
som mutex i ettsynchronized
block, - gjutning av en
null
objektreferens, - uppackning en
null
objektreferens, och - kasta en
null
objektreferens.
De vanligaste grundorsakerna för NPE:
- glömmer att initiera ett fält med en referenstyp,
- glömmer att initiera element i en matris av en referenstyp, eller
- inte testa resultaten av vissa API-metoder som specificeras som återlämnande av
null
under vissa omständigheter.
Exempel på vanliga metoder som returnerar null
inkluderar:
- Metoden
get(key)
iMap
API kommer att returnera ennull
om du kallar den med en nyckel som inte har en mappning. -
getResource(path)
ochgetResourceAsStream(path)
iClassLoader
ochClass
API kommer att returneranull
om resursen inte kan hittas. - Metoden
get()
iReference
API kommer att returneranull
om skräpfångaren har rensat referensen. - Olika
getXxxx
metoder i Java EE-servlet-API: erna kommer att returneranull
om du försöker hämta en icke-existerande förfrågningsparameter, session eller sessionattribut och så vidare.
Det finns strategier för att undvika oönskade NPE: er, till exempel uttryckligen testa för null
eller använda "Yoda Notation", men dessa strategier har ofta det oönskade resultatet av att dölja problem i din kod som verkligen borde fixas.
Fallgrop - Onödig användning av primitiva omslag kan leda till NullPointerExceptions
Ibland kommer programmerare som är nya Java att använda primitiva typer och omslag omväxlande. Detta kan leda till problem. Tänk på detta exempel:
public class MyRecord {
public int a, b;
public Integer c, d;
}
...
MyRecord record = new MyRecord();
record.a = 1; // OK
record.b = record.b + 1; // OK
record.c = 1; // OK
record.d = record.d + 1; // throws a NullPointerException
Vår MyRecord
klass 1 förlitar sig på standardinitialisering för att initialisera värdena i dess fält. Således, när vi new
en post, kommer a
och b
fälten att ställas in på noll, och c
och d
fälten kommer att ställas in på null
.
När vi försöker använda standardinitierade fält ser vi att int
fälten fungerar hela tiden, men Integer
fungerar i vissa fall och inte andra. Specifikt, i fallet som misslyckas (med d
), är det som händer att uttrycket på höger sida försöker ta bort en null
, och det är det som gör att NullPointerException
kastas.
Det finns några sätt att titta på detta:
Om fälten
c
ochd
behöver vara primitiva omslag, borde vi inte heller lita på standardinitialisering, eller så bör vi testa förnull
. För först är det korrekta tillvägagångssättet såvida det inte finns en bestämd betydelse för fälten inull
.Om fälten inte behöver vara primitiva omslag, är det ett misstag att göra dem till primitiva omslag. Utöver detta problem har de primitiva omslagen extra omkostnader relativt primitiva typer.
Lektionen här är att inte använda primitiva omslagstyper om du inte behöver det.
1 - Denna klass är inte ett exempel på god kodningssed. Till exempel skulle en väl utformad klass inte ha offentliga fält. Detta är emellertid inte poängen med detta exempel.
Fallgrop - Använd null för att representera en tom matris eller samling
Vissa programmerare tycker att det är en bra idé att spara utrymme genom att använda en null
att representera en tom matris eller samling. Även om det är sant att du kan spara en liten mängd utrymme, är flipsiden att den gör din kod mer komplicerad och mer ömtålig. Jämför dessa två versioner av en metod för att summera en matris:
Den första versionen är hur du normalt kodar metoden:
/**
* Sum the values in an array of integers.
* @arg values the array to be summed
* @return the sum
**/
public int sum(int[] values) {
int sum = 0;
for (int value : values) {
sum += value;
}
return sum;
}
Den andra versionen är hur du måste koda metoden om du brukar null
att representera en tom matris.
/**
* Sum the values in an array of integers.
* @arg values the array to be summed, or null.
* @return the sum, or zero if the array is null.
**/
public int sum(int[] values) {
int sum = 0;
if (values != null) {
for (int value : values) {
sum += value;
}
}
return sum;
}
Som ni ser är koden lite mer komplicerad. Detta kan direkt hänföras till beslutet att använda null
på detta sätt.
Tänk nu på om det här arrayet som kan vara ett null
används på många platser. På varje plats där du använder det måste du överväga om du måste testa för null
. Om du missar ett null
som måste finnas där riskerar du en NullPointerException
. Därför leder strategin att använda null
på detta sätt till att din ansökan blir mer ömtålig; dvs mer sårbara för konsekvenserna av programmeringsfel.
Lektionen här är att använda tomma matriser och tomma listor när det är vad du menar.
int[] values = new int[0]; // always empty
List<Integer> list = new ArrayList(); // initially empty
List<Integer> list = Collections.emptyList(); // always empty
Utrymmet ovanför är litet, och det finns andra sätt att minimera det om detta är en värdefull sak att göra.
Fallgrop - "Att göra bra" oväntade nollor
På StackOverflow ser vi ofta kod som denna i Answers:
public String joinStrings(String a, String b) {
if (a == null) {
a = "";
}
if (b == null) {
b = "";
}
return a + ": " + b;
}
Ofta åtföljs detta av en påstående som är "bästa praxis" att testa för null
som detta för att undvika NullPointerException
.
Är det bästa praxis? Kort sagt: Nej
Det finns några underliggande antaganden som måste ifrågasättas innan vi kan säga om det är en bra idé att göra detta i våra joinStrings
:
Vad betyder det att "a" eller "b" är noll?
Ett String
kan vara noll eller fler tecken, så vi har redan ett sätt att representera en tom sträng. Betyder null
något annat än ""
? Om nej, är det problematiskt att ha två sätt att representera en tom sträng.
Kom nollet från en oinitialiserad variabel?
En null
kan komma från ett oinitialiserat fält eller ett oinitialiserat arrayelement. Värdet kan uninitialiseras av design eller av misstag. Om det var av misstag så är detta ett fel.
Representerar nollet "vet inte" eller "saknas värde"?
Ibland kan en null
ha en äkta betydelse; t.ex. att det verkliga värdet på en variabel är okänt eller otillgängligt eller "valfritt". I Java 8 ger klassen Optional
ett bättre sätt att uttrycka det.
Om detta är ett fel (eller ett designfel) ska vi "göra det bra"?
En tolkning av koden är att vi "gör bra" till en oväntad null
genom att använda en tom sträng i stället. Är rätt strategi? Skulle det vara bättre att låta NullPointerException
hända och sedan fånga undantaget längre upp i bunten och logga in det som ett fel?
Problemet med "att göra bra" är att det kan antingen dölja problemet eller göra det svårare att diagnostisera.
Är detta effektivt / bra för kodkvalitet?
Om metoden "gör bra" används konsekvent kommer din kod att innehålla många "defensiva" nolltester. Detta kommer att göra det längre och svårare att läsa. Dessutom kan allt detta testa och "göra bra" påverka resultatet för din ansökan.
Sammanfattningsvis
Om null
är ett meningsfullt värde är testet för null
rätt tillvägagångssätt. Sammanfattningen är att om ett null
är meningsfullt, bör detta tydligt dokumenteras i javadokorna för alla metoder som accepterar null
eller returnerar det.
Annars är det en bättre idé att behandla en oväntad null
som ett programmeringsfel och låta NullPointerException
hända så att utvecklaren får veta att det finns ett problem i koden.
Fallgrop - Återvändning av noll istället för att kasta ett undantag
Vissa Java-programmerare har en generell motvilja mot att kasta eller sprida undantag. Detta leder till kod som följande:
public Reader getReader(String pathname) {
try {
return new BufferedReader(FileReader(pathname));
} catch (IOException ex) {
System.out.println("Open failed: " + ex.getMessage());
return null;
}
}
Så vad är problemet med det?
Problemet är att getReader
returnerar ett null
som specialvärde för att indikera att Reader
inte kunde öppnas. Nu måste det returnerade värdet testas för att se om det är null
innan det används. Om testet utelämnas blir resultatet en NullPointerException
.
Det finns faktiskt tre problem här:
-
IOException
fångades för snart. - Strukturen för denna kod innebär att det finns en risk att läcka en resurs.
- En
null
användes sedan tillbaka eftersom ingen "riktig"Reader
var tillgänglig att returnera.
Om man antar att undantaget behövde fångas tidigt så här fanns det ett par alternativ till att återvända null
:
- Det skulle vara möjligt att implementera en
NullReader
klass; t.ex. en där API: s operationer uppträder som om läsaren redan befann sig i "slutet av fil" -positionen. - Med Java 8 skulle det vara möjligt att förklara
getReader
som att returnera enOptional<Reader>
.
Fallgrop - Kontrollerar inte om en I / O-ström inte ens initialiseras när den stängs
För att förhindra minnesläckage bör man inte glömma att stänga en ingångsström eller en utgångsström vars jobb är gjort. Detta görs vanligtvis med en try
- catch
- finally
uttalande utan catch
:
void writeNullBytesToAFile(int count, String filename) throws IOException {
FileOutputStream out = null;
try {
out = new FileOutputStream(filename);
for(; count > 0; count--)
out.write(0);
} finally {
out.close();
}
}
Även om koden ovan kan se oskyldig ut, har den en brist som kan göra felsökning omöjlig. Om raden där out
initieras ( out = new FileOutputStream(filename)
) kastar ett undantag, kommer out
att vara null
när out.close()
körs, vilket resulterar i en otäck NullPointerException
!
För att förhindra detta, helt enkelt se till att strömmen inte är null
innan du försöker stänga den.
void writeNullBytesToAFile(int count, String filename) throws IOException {
FileOutputStream out = null;
try {
out = new FileOutputStream(filename);
for(; count > 0; count--)
out.write(0);
} finally {
if (out != null)
out.close();
}
}
En ännu bättre metod är att try
med-resurser, eftersom det automatiskt kommer att stänga strömmen med en sannolikhet på 0 för att kasta en NPE utan behov av ett finally
block.
void writeNullBytesToAFile(int count, String filename) throws IOException {
try (FileOutputStream out = new FileOutputStream(filename)) {
for(; count > 0; count--)
out.write(0);
}
}
Fallgrop - Använd "Yoda-notation" för att undvika NullPointerException
Många exempelkoder som publiceras på StackOverflow innehåller utdrag som denna:
if ("A".equals(someString)) {
// do something
}
Detta "förhindrar" eller "undviker" en möjlig NullPointerException
i fallet att someString
är null
. Det kan dessutom diskuteras
"A".equals(someString)
är bättre än:
someString != null && someString.equals("A")
(Det är mer kortfattat, och under vissa omständigheter kan det vara mer effektivt. Men som vi hävdar nedan, kan sammanfattning vara negativ.)
Men den verkliga fallgropen använder Yoda-testet för att undvika NullPointerExceptions
som vana.
När du skriver "A".equals(someString)
faktiskt" gör du bra "fallet där someString
råkar vara null
. Men som ett annat exempel ( Pitfall - "Att göra bra" oväntade noll ) förklarar, "att göra bra" null
kan vara skadligt av olika skäl.
Detta innebär att Yoda-förhållandena inte är "bästa praxis" 1 . Om inte null
förväntas, är det bättre att låta NullPointerException
hända så att du kan få ett enhetstestfel (eller en felrapport). Det gör att du kan hitta och fixa felet som orsakade att det oväntade / oönskade null
visades.
Yoda-villkor bör endast användas i fall där null
förväntas eftersom objektet du testar kommer från ett API som är dokumenterat att returnera ett null
. Och utan tvekan kan det vara bättre att använda ett av de mindre vackra sätten att uttrycka testet eftersom det hjälper till att markera null
för någon som granskar din kod.
1 - Enligt Wikipedia : "Bästa kodningspraxis är en uppsättning informella regler som mjukvaruutvecklingssamhället har lärt sig över tid som kan bidra till att förbättra programvarans kvalitet." . Att använda Yoda notation uppnår inte detta. I många situationer förvärrar det koden.