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 ett synchronized 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) i Map API kommer att returnera en null om du kallar den med en nyckel som inte har en mappning.
  • getResource(path) och getResourceAsStream(path) i ClassLoader och Class API kommer att returnera null om resursen inte kan hittas.
  • Metoden get() i Reference API kommer att returnera null om skräpfångaren har rensat referensen.
  • Olika getXxxx metoder i Java EE-servlet-API: erna kommer att returnera null 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 och d behöver vara primitiva omslag, borde vi inte heller lita på standardinitialisering, eller så bör vi testa för null . För först är det korrekta tillvägagångssättet såvida det inte finns en bestämd betydelse för fälten i null .

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

  1. IOException fångades för snart.
  2. Strukturen för denna kod innebär att det finns en risk att läcka en resurs.
  3. 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 :

  1. 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.
  2. Med Java 8 skulle det vara möjligt att förklara getReader som att returnera en Optional<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.



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow