Java Language
Java Pitfalls - Nulls en NullPointerException
Zoeken…
Opmerkingen
De waarde null
is de standaardwaarde voor een niet-geïnitialiseerde waarde van een veld waarvan het type een referentietype is.
NullPointerException
(of NPE) is de uitzondering die wordt gegenereerd wanneer u probeert een ongepaste bewerking uit te voeren op de null
objectreferentie. Dergelijke operaties omvatten:
- een instantiemethode aanroepen op een
null
doelobject, - toegang tot een veld van een
null
, - proberen een
null
arrayobject te indexeren of toegang te krijgen tot de lengte ervan, - met behulp van een
null
als de mutex in eensynchronized
blok, - een
null
casten, - een
null
objectreferentie uitpakken, en - een
null
werpen.
De meest voorkomende grondoorzaken voor NPE's:
- vergeten om een veld met een referentietype te initialiseren,
- vergeten elementen van een array van een referentietype te initialiseren, of
- niet het testen van de resultaten van bepaalde API-methoden die zijn gespecificeerd als '
null
retourneren' in bepaalde omstandigheden.
Voorbeelden van veelgebruikte methoden die null
retourneren zijn:
- De methode
get(key)
in deMap
API retourneert eennull
als u deze aanroept met een sleutel die geen toewijzing heeft. - De
getResource(path)
engetResourceAsStream(path)
in deClassLoader
enClass
API's retournerennull
als de bron niet kan worden gevonden. - De methode
get()
in deReference
API retourneertnull
als de vuilnisman de referentie heeft gewist. - Verschillende
getXxxx
methoden in de Java EE-servlet-API's retournerennull
als u probeert een niet-bestaande aanvraagparameter, sessie ofgetXxxx
halen, enzovoort.
Er zijn strategieën om ongewenste NPE's te vermijden, zoals expliciet testen op null
of het gebruik van "Yoda Notation", maar deze strategieën hebben vaak het ongewenste resultaat van het verbergen van problemen in uw code die echt moeten worden opgelost.
Valkuil - Onnodig gebruik van Primitive Wrappers kan leiden tot NullPointerExceptions
Soms zullen programmeurs die nieuw Java zijn, primitieve typen en wrappers door elkaar gebruiken. Dit kan tot problemen leiden. Beschouw dit voorbeeld:
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
Onze MyRecord
klasse 1 vertrouwt op standaardinitialisatie om de waarden in de velden te initialiseren. Wanneer we dus een record new
maken, worden de velden a
en b
op nul gezet en worden de velden c
en d
op null
.
Wanneer we proberen de standaard geïnitialiseerde velden te gebruiken, zien we dat de int
velden altijd werken, maar de velden voor gehele Integer
werken in sommige gevallen en andere niet. Specifiek in het geval dat niet (met d
), wat gebeurt dat de expressie aan de rechterzijde pogingen om een unbox null
referentie, en dat is wat de oorzaken NullPointerException
worden geworpen.
Er zijn een aantal manieren om hiernaar te kijken:
Als de velden
c
end
primitieve wrappers moeten zijn, dan zouden we niet moeten vertrouwen op standaardinitialisatie, of moeten we testen opnull
. Voor eerstgenoemde is de juiste benadering tenzij er een duidelijke betekenis is voor de velden in denull
.Als de velden geen primitieve wrappers hoeven te zijn, is het een vergissing om ze primitieve wrappers te maken. Naast dit probleem hebben de primitieve wikkels extra overhead ten opzichte van primitieve typen.
De les hier is om geen primitieve wikkeltypen te gebruiken, tenzij het echt nodig is.
1 - Deze klasse is geen voorbeeld van goede codeerpraktijken. Een goed ontworpen klasse zou bijvoorbeeld geen openbare velden hebben. Dat is echter niet de bedoeling van dit voorbeeld.
Valkuil - Null gebruiken om een lege array of verzameling weer te geven
Sommige programmeurs denken dat het een goed idee is om ruimte te besparen door een null
te gebruiken om een lege array of verzameling weer te geven. Hoewel het waar is dat je een kleine hoeveelheid ruimte kunt besparen, is de keerzijde dat het je code ingewikkelder en kwetsbaarder maakt. Vergelijk deze twee versies van een methode voor het samenvatten van een array:
De eerste versie is hoe je normaal de methode zou coderen:
/**
* 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;
}
De tweede versie is hoe je de methode moet coderen als je de gewoonte hebt om null
te gebruiken om een lege array weer te geven.
/**
* 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;
}
Zoals u ziet, is de code een beetje ingewikkelder. Dit is direct te wijten aan de beslissing om null
op deze manier te gebruiken.
Overweeg nu of deze array die op null
kan staan op veel plaatsen wordt gebruikt. Op elke plaats waar u het gebruikt, moet u overwegen of u op null
moet testen. Als je een null
test mist die daar moet zijn, NullPointerException
je een NullPointerException
. Vandaar dat de strategie om null
op deze manier te gebruiken ertoe leidt dat uw toepassing fragieler wordt; dwz kwetsbaarder voor de gevolgen van programmeerfouten.
De les hier is om lege arrays en lege lijsten te gebruiken als dat is wat je bedoelt.
int[] values = new int[0]; // always empty
List<Integer> list = new ArrayList(); // initially empty
List<Integer> list = Collections.emptyList(); // always empty
De ruimte boven het hoofd is klein en er zijn andere manieren om het te minimaliseren als dit de moeite waard is om te doen.
Valkuil - "Goedmaken" onverwachte nulwaarden
Op StackOverflow zien we deze code vaak in Antwoorden:
public String joinStrings(String a, String b) {
if (a == null) {
a = "";
}
if (b == null) {
b = "";
}
return a + ": " + b;
}
Vaak gaat dit gepaard met een bewering die "best practice" is om op null
te testen zoals deze om NullPointerException
te voorkomen.
Is het een goede praktijk? Kortom: nee
Er zijn enkele onderliggende aannames die in twijfel moeten worden joinStrings
voordat we kunnen zeggen of het een goed idee is om dit te doen in onze joinStrings
:
Wat betekent "a" of "b" om nul te zijn?
Een String
kan nul of meer tekens zijn, dus we hebben al een manier om een lege tekenreeks weer te geven. Betekent null
iets anders dan ""
? Zo nee, dan is het problematisch om twee manieren te hebben om een lege string weer te geven.
Kwam de nul uit een niet-geïnitialiseerde variabele?
Een null
kan afkomstig zijn van een niet-geïnitialiseerd veld of een niet-geïnitialiseerd array-element. De waarde kan niet worden geïnitialiseerd door ontwerp of per ongeluk. Als het per ongeluk was, is dit een bug.
Vertegenwoordigt de nul een "weet niet" of "ontbrekende waarde"?
Soms kan een null
een echte betekenis hebben; bijv. dat de werkelijke waarde van een variabele onbekend of niet beschikbaar of "optioneel" is. In Java 8, de Optional
klasse zorgt voor een betere manier van uitdrukken dat.
Als dit een fout is (of een ontwerpfout), moeten we dan "goedmaken"?
Een interpretatie van de code is dat we een onverwachte null
" null
door in plaats daarvan een lege string te gebruiken. Is de juiste strategie? Zou het beter zijn om de NullPointerException
te laten gebeuren en vervolgens de uitzondering verder op de stapel te vangen en als een bug te registreren?
Het probleem met "goedmaken" is dat het het probleem kan verbergen of moeilijker kan diagnosticeren.
Is dit efficiënt / goed voor codekwaliteit?
Als de "make good" -benadering consistent wordt gebruikt, zal uw code veel "defensieve" nul-tests bevatten. Dit zal het langer en moeilijker maken om te lezen. Bovendien kan al dit testen en 'goedmaken' gevolgen hebben voor de prestaties van uw toepassing.
samengevat
Als null
een betekenisvolle waarde is, is testen voor het null
geval de juiste aanpak. Het logische gevolg is dat als een null
betekenisvol is, dit duidelijk moet worden gedocumenteerd in de javadocs van alle methoden die de null
accepteren of retourneren.
Anders is het een beter idee om een onverwachte null
als een programmeerfout te behandelen en de NullPointerException
gebeuren zodat de ontwikkelaar te weten komt dat er een probleem is in de code.
Valkuil - Nul retourneren in plaats van een uitzondering te gooien
Sommige Java-programmeurs hebben een algemene afkeer van het gooien of verspreiden van uitzonderingen. Dit leidt tot code zoals de volgende:
public Reader getReader(String pathname) {
try {
return new BufferedReader(FileReader(pathname));
} catch (IOException ex) {
System.out.println("Open failed: " + ex.getMessage());
return null;
}
}
Dus wat is daar het probleem mee?
Het probleem is dat de getReader
een null
retourneert als een speciale waarde om aan te geven dat de Reader
niet kon worden geopend. Nu moet de geretourneerde waarde worden getest om te zien of deze null
voordat deze wordt gebruikt. Als de test wordt weggelaten, is het resultaat een NullPointerException
.
Er zijn hier eigenlijk drie problemen:
- De
IOException
werd te snel betrapt. - De structuur van deze code betekent dat er een risico bestaat dat een bron lekt.
- Er is een
null
gebruikt en vervolgens geretourneerd omdat er geen "echte"Reader
beschikbaar was om terug te keren.
Ervan uitgaande dat de uitzondering zo vroeg moest worden opgevangen, waren er zelfs een aantal alternatieven voor het terugkeren van null
:
- Het zou mogelijk zijn om een
NullReader
klasse te implementeren; bijvoorbeeld een waarbij de API-bewerkingen zich gedragen alsof de lezer zich al op de positie "einde bestand" bevindt. - Met Java 8 zou het mogelijk zijn om
getReader
te declareren als eenOptional<Reader>
retourneren.
Valkuil - Niet controleren of een I / O-stream niet eens wordt geïnitialiseerd bij het afsluiten
Om geheugenlekken te voorkomen, moet je niet vergeten een invoerstroom of een uitvoerstroom te sluiten waarvan het werk is gedaan. Dit wordt meestal gedaan met een try
- catch
- finally
statement zonder het catch
gedeelte:
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();
}
}
Hoewel de bovenstaande code onschuldig lijkt, heeft deze een fout waardoor foutopsporing onmogelijk is. Als de regel waar out
is geïnitialiseerd ( out = new FileOutputStream(filename)
) een uitzondering out.close()
, wordt out
null
wanneer out.close()
wordt uitgevoerd, wat resulteert in een vervelende NullPointerException
!
Om dit te voorkomen, zorgt u ervoor dat de stream niet null
voordat u deze probeert te sluiten.
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();
}
}
Een nog betere aanpak is om -met-resources te try
, omdat het automatisch de stream sluit met een waarschijnlijkheid van 0 om een NPE te gooien zonder dat er finally
blokkade nodig is.
void writeNullBytesToAFile(int count, String filename) throws IOException {
try (FileOutputStream out = new FileOutputStream(filename)) {
for(; count > 0; count--)
out.write(0);
}
}
Valkuil - Gebruik van "Yoda-notatie" om NullPointerException te voorkomen
Veel voorbeeldcode die op StackOverflow wordt gepost, bevat fragmenten zoals deze:
if ("A".equals(someString)) {
// do something
}
Dit "voorkomt" of "vermijdt" een mogelijke NullPointerException
in het geval dat someString
null
. Bovendien is dat betwistbaar
"A".equals(someString)
is beter dan:
someString != null && someString.equals("A")
(Het is beknopter en in sommige omstandigheden kan het efficiënter zijn. Echter, zoals we hieronder beargumenteren, kan beknoptheid negatief zijn.)
De echte valkuil is echter het gebruik van de Yoda-test om NullPointerExceptions
uit gewoonte te vermijden .
Wanneer u "A".equals(someString)
"maakt u het geval" waar someString
toevallig null
. Maar zoals een ander voorbeeld ( Pitfall - "Goedmaken" onverwachte nulwaarden ) verklaart, " goedmaken " null
kan om verschillende redenen schadelijk zijn.
Dit betekent dat Yoda-omstandigheden geen "beste praktijk" zijn 1 . Tenzij de null
wordt verwacht, is het beter om de NullPointerException
te laten gebeuren, zodat u een mislukte unit-test (of een bugrapport) kunt krijgen. Hiermee kunt u de bug vinden en oplossen die de onverwachte / ongewenste null
veroorzaakt.
Yoda-omstandigheden mogen alleen worden gebruikt in gevallen waarin de null
wordt verwacht, omdat het object dat u test afkomstig is van een API waarvan is gedocumenteerd dat deze een null
retourneert. En misschien is het beter om een van de minder mooie manieren te gebruiken om de test uit te drukken, omdat dat helpt om de null
test te benadrukken aan iemand die je code aan het herzien is.
1 - Volgens Wikipedia : "De beste coderingsmethoden zijn een aantal informele regels die de gemeenschap voor softwareontwikkeling in de loop van de tijd heeft geleerd en die kan helpen de kwaliteit van software te verbeteren." . Met Yoda-notatie wordt dit niet bereikt. In veel situaties maakt het de code erger.