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 een synchronized 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 de Map API retourneert een null als u deze aanroept met een sleutel die geen toewijzing heeft.
  • De getResource(path) en getResourceAsStream(path) in de ClassLoader en Class API's retourneren null als de bron niet kan worden gevonden.
  • De methode get() in de Reference API retourneert null als de vuilnisman de referentie heeft gewist.
  • Verschillende getXxxx methoden in de Java EE-servlet-API's retourneren null als u probeert een niet-bestaande aanvraagparameter, sessie of getXxxx 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 en d primitieve wrappers moeten zijn, dan zouden we niet moeten vertrouwen op standaardinitialisatie, of moeten we testen op null . Voor eerstgenoemde is de juiste benadering tenzij er een duidelijke betekenis is voor de velden in de null .

  • 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:

  1. De IOException werd te snel betrapt.
  2. De structuur van deze code betekent dat er een risico bestaat dat een bron lekt.
  3. 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 :

  1. 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.
  2. Met Java 8 zou het mogelijk zijn om getReader te declareren als een Optional<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.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow