undgå Check for Null-sætning i Java
oversigt
generelt er nulvariabler, referencer og samlinger vanskelige at håndtere i Java-kode. Ikke alene er de svære at identificere, men de er også komplekse at håndtere.
faktisk kan enhver miss i forbindelse med null ikke identificeres på kompileringstidspunktet og resulterer i en Nullpointerundtagelse ved kørsel.
i denne tutorial vil vi se på behovet for at kontrollere for null i Java og forskellige alternativer, der hjælper os med at undgå null-kontrol i vores kode.
Further reading:
Using NullAway to Avoid NullPointerExceptions
Spring Null-Safety Annotations
Introduction to the Null Object Pattern
What Is NullPointerException?
i henhold til Javadoc for Nullpointerundtagelse kastes det, når en applikation forsøger at bruge null i et tilfælde, hvor et objekt er påkrævet, såsom:
- kalder en forekomstmetode for et null-objekt
- adgang til eller ændring af et felt af et null-objekt
- tager længden af null, som om det var et array
- adgang til eller ændring af slots af null, som om det var et array
- null som om det var en kastbar værdi
lad os hurtigt se et par eksempler på Java-koden, der forårsager denne undtagelse:
public void doSomething() { String result = doSomethingElse(); if (result.equalsIgnoreCase("Success")) // success }}private String doSomethingElse() { return null;}
Her forsøgte vi at påberåbe sig et metodeopkald til en null reference. Dette ville resultere i en undtagelse fra Nullpointer.
et andet almindeligt eksempel er, hvis vi forsøger at få adgang til et null-array:
public static void main(String args) { findMax(null);}private static void findMax(int arr) { int max = arr; //check other elements in loop}
dette forårsager en Nullpointerundtagelse på linje 6.
adgang til ethvert felt, metode eller indeks for et null-objekt forårsager således en Nullpointerundtagelse, som det kan ses af eksemplerne ovenfor.
en almindelig måde at undgå Nullpointerundtagelse er at kontrollere for null:
public void doSomething() { String result = doSomethingElse(); if (result != null && result.equalsIgnoreCase("Success")) { // success } else // failure}private String doSomethingElse() { return null;}
i den virkelige verden har programmører svært ved at identificere, hvilke objekter der kan være null. En aggressiv sikker strategi kan være at kontrollere null for hvert objekt. Dette forårsager dog mange overflødige nulkontrol og gør vores kode mindre læsbar.
i de næste par afsnit gennemgår vi nogle af alternativerne i Java, der undgår sådan redundans.
håndtering af nul gennem API-kontrakten
som diskuteret i det sidste afsnit forårsager adgang til metoder eller variabler af null-objekter en Nullpointerundtagelse. Vi diskuterede også, at sætte en null kontrol på et objekt, før adgang til det eliminerer muligheden for Nullpointerundtagelse.
der er dog ofte API ‘ er, der kan håndtere null-værdier. For eksempel:
public void print(Object param) { System.out.println("Printing " + param);}public Object process() throws Exception { Object result = doSomething(); if (result == null) { throw new Exception("Processing fail. Got a null response"); } else { return result; }}
udskrivningsmetoden() ville bare udskrive “null”, men vil ikke smide en undtagelse. Tilsvarende vil process () aldrig returnere null i sit svar. Det kaster snarere en undtagelse.
så for en klientkode, der får adgang til ovenstående API ‘ er, er der ikke behov for en null-kontrol.
sådanne API ‘ er skal dog gøre det eksplicit i deres kontrakt. Et fælles sted for API ‘ er at offentliggøre en sådan kontrakt er JavaDoc.
dette giver dog ingen klar indikation af API-kontrakten og er således afhængig af klientkodeudviklerne for at sikre dens overholdelse.
i det næste afsnit ser vi, hvordan et par ide ‘ er og andre udviklingsværktøjer hjælper udviklere med dette.
automatisering af API-kontrakter
4.1. Brug af statisk kodeanalyse
statiske kodeanalyseværktøjer hjælper med at forbedre kodekvaliteten til en hel del. Og et par sådanne værktøjer giver også udviklerne mulighed for at opretholde nulkontrakten. Et eksempel er FindBugs.
FindBugs hjælper med at styre null kontrakt gennem @Nullable og @NonNull anmærkninger. Vi kan bruge disse kommentarer over enhver metode, felt, lokal variabel eller parameter. Dette gør det eksplicit for klientkoden, om den annoterede type kan være null eller ej. Lad os se et eksempel:
public void accept(@Nonnull Object param) { System.out.println(param.toString());}
Her gør @NonNull det klart, at argumentet ikke kan være null. Hvis klientkoden kalder denne metode uden at kontrollere argumentet for null, ville FindBugs generere en advarsel på kompileringstidspunktet.
4.2. Brug af IDE-Support
udviklere er generelt afhængige af IDE ‘ er til at skrive Java-kode. Og funktioner som smart kode færdiggørelse og nyttige advarsler, som når en variabel måske ikke er blevet tildelt, hjælper helt sikkert i høj grad.
nogle ide ‘ er giver også udviklere mulighed for at styre API-kontrakter og derved eliminere behovet for et statisk kodeanalyseværktøj. IntelliJ IDEA giver @NonNull og @Nullable anmærkninger. For at tilføje understøttelsen til disse kommentarer i IntelliJ skal vi tilføje følgende Maven-afhængighed:
<dependency> <groupId>org.jetbrains</groupId> <artifactId>annotations</artifactId> <version>16.0.2</version></dependency>
nu vil IntelliJ generere en advarsel, hvis null-kontrollen mangler, som i vores sidste eksempel.
IntelliJ leverer også en kontraktanmærkning til håndtering af komplekse API-kontrakter.
5. Påstande
indtil nu har vi kun talt om at fjerne behovet for null-kontrol fra klientkoden. Men det er sjældent anvendeligt i virkelige applikationer.
lad os nu antage, at vi arbejder med en API, der ikke kan acceptere null-parametre eller kan returnere et null-svar, der skal håndteres af klienten. Dette præsenterer behovet for os at kontrollere parametrene eller svaret for en null-værdi.
Her kan vi bruge Java-påstande i stedet for den traditionelle betingede Erklæring om nulkontrol:
public void accept(Object param){ assert param != null; doSomething(param);}
i linje 2 kontrollerer vi for en null-parameter. Hvis påstandene er aktiveret, ville dette resultere i en AssertionError.
selvom det er en god måde at hævde forudsætninger som ikke-null-parametre, har denne tilgang to store problemer:
- påstande er normalt deaktiveret i en JVM
- en falsk påstand resulterer i en ukontrolleret fejl, der er uoprettelig
derfor anbefales det ikke, at programmører bruger påstande til kontrol af betingelser. I de følgende afsnit vil vi diskutere andre måder at håndtere null valideringer.
undgå nul kontrol gennem kodning praksis
6.1. Forudsætninger
det er normalt en god praksis at skrive kode, der fejler tidligt. Derfor, hvis en API accepterer flere parametre, der ikke er tilladt at være null, er det bedre at kontrollere for hver IKKE-null parameter som en forudsætning for API.
lad os for eksempel se på to metoder – en der fejler tidligt, og en der ikke gør det:
public void goodAccept(String one, String two, String three) { if (one == null || two == null || three == null) { throw new IllegalArgumentException(); } process(one); process(two); process(three);}public void badAccept(String one, String two, String three) { if (one == null) { throw new IllegalArgumentException(); } else { process(one); } if (two == null) { throw new IllegalArgumentException(); } else { process(two); } if (three == null) { throw new IllegalArgumentException(); } else { process(three); }}
det er klart, at vi foretrækker goodAccept() frem for badAccept().
som et alternativ kan vi også bruge guavas forudsætninger for validering af API-parametre.
6. 2. Brug af primitiver i stedet for Indpakningsklasser
da null ikke er en acceptabel værdi for primitiver som int, bør vi foretrække dem frem for deres indpakningsmodeller som heltal, hvor det er muligt.
overvej to implementeringer af en metode, der opsummerer to heltal:
public static int primitiveSum(int a, int b) { return a + b;}public static Integer wrapperSum(Integer a, Integer b) { return a + b;}
lad os nu kalde disse API ‘ er i vores klientkode:
int sum = primitiveSum(null, 2);
dette ville resultere i en kompileringstidsfejl, da null ikke er en gyldig værdi for en int.
og når du bruger API med indpakningsklasser, får vi en Nullpointerundtagelse:
assertThrows(NullPointerException.class, () -> wrapperSum(null, 2));
Der er også andre faktorer for at bruge primitiver over indpakninger, som vi dækkede i en anden tutorial, Java primitiver versus objekter.
6, 3. Tomme samlinger
lejlighedsvis skal vi returnere en samling som svar fra en metode. For sådanne metoder bør vi altid forsøge at returnere en tom samling i stedet for null:
public List<String> names() { if (userExists()) { return Stream.of(readName()).collect(Collectors.toList()); } else { return Collections.emptyList(); }}
derfor har vi undgået behovet for vores klient til at udføre en null check, når du kalder denne metode.
brug af objekter
Java 7 introducerede den nye Objects API. Denne API har flere statiske nyttemetoder, der fjerner en masse overflødig kode. Lad os se på en sådan metode, krævernull():
public void accept(Object param) { Objects.requireNonNull(param); // doSomething()}
lad os nu teste accept () – metoden:
assertThrows(NullPointerException.class, () -> accept(null));
så hvis null er bestået som et argument, accepterer() kaster en Nullpointerundtagelse.
denne klasse har også isnull() og nonNull () metoder, der kan bruges som prædikater til at kontrollere et objekt for null.
brug af valgfri
8.1. Ved hjælp af orelsekast
Java 8 introduceret en ny valgfri API på sproget. Dette giver en bedre kontrakt til håndtering af valgfrie værdier sammenlignet med null. Lad os se, hvordan valgfri fjerner behovet for nulkontrol:
public Optional<Object> process(boolean processed) { String response = doSomething(processed); if (response == null) { return Optional.empty(); } return Optional.of(response);}private String doSomething(boolean processed) { if (processed) { return "passed"; } else { return null; }}
Ved at returnere en valgfri, som vist ovenfor, gør procesmetoden det klart for den, der ringer op, at svaret kan være tomt og skal håndteres på kompileringstidspunktet.
dette fjerner især behovet for null-kontrol i klientkoden. Et tomt svar kan håndteres forskelligt ved hjælp af den deklarative stil for den valgfri API:
assertThrows(Exception.class, () -> process(false).orElseThrow(() -> new Exception()));
desuden giver det også en bedre kontrakt til API-udviklere for at betegne klienterne, at en API kan returnere et tomt svar.
selvom vi eliminerede behovet for en null check på opkalderen af denne API, brugte vi den til at returnere et tomt svar. For at undgå dette giver valgfri en ofNullable metode, der returnerer en valgfri med den angivne værdi eller tom, hvis værdien er null:
public Optional<Object> process(boolean processed) { String response = doSomething(processed); return Optional.ofNullable(response);}
8.2. Brug Valgfri med samlinger
mens der beskæftiger sig med tomme samlinger, valgfri kommer i handy:
public String findFirst() { return getList().stream() .findFirst() .orElse(DEFAULT_VALUE);}
denne funktion skal returnere det første element i en liste. Stream API ‘ s findFirst-funktion returnerer en tom valgfri, når der ikke er nogen data. Her har vi brugt orElse til at give en standardværdi i stedet.
Dette giver os mulighed for at håndtere enten tomme lister eller lister, som efter at vi har brugt Streambibliotekets filtermetode, ikke har nogen varer at levere.
Alternativt kan vi også give klienten mulighed for at beslutte, hvordan man håndterer tom ved at returnere valgfri fra denne metode:
public Optional<String> findOptionalFirst() { return getList().stream() .findFirst();}
derfor, hvis resultatet af getList er tomt, returnerer denne metode en tom valgfri til klienten.
brug af valgfri med samlinger giver os mulighed for at designe API ‘ er, der helt sikkert returnerer ikke-null-værdier, hvilket undgår eksplicit null-kontrol af klienten.
det er vigtigt at bemærke her, at denne implementering er afhængig af, at getList ikke returnerer null. Men som vi diskuterede i sidste afsnit, er det ofte bedre at returnere en tom liste snarere end en null.
8, 3. Kombination af Optionals
Når vi begynder at få vores funktioner til at returnere valgfri, har vi brug for en måde at kombinere deres resultater til en enkelt værdi. Lad os tage vores getList eksempel fra tidligere. Hvad hvis det skulle returnere en valgfri liste, eller skulle pakkes med en metode, der indpakket en null med valgfri ved hjælp af ofNullable?
vores findFirst-metode ønsker at returnere et valgfrit første element i en valgfri liste:
public Optional<String> optionalListFirst() { return getOptionalList() .flatMap(list -> list.stream().findFirst());}
Ved at bruge flatMap-funktionen på den valgfri returnerede fra getOptional kan vi pakke resultatet af et indre udtryk, der returnerer Valgfrit. Uden flatMap ville resultatet være valgfrit<Valgfrit<String>>. FlatMap-operationen udføres kun, når den valgfri ikke er tom.
biblioteker
9.1. Brug af Lombok
Lombok er et fantastisk bibliotek, der reducerer mængden af kedelpladekode i vores projekter. Den leveres med et sæt kommentarer, der træder i stedet for almindelige dele af kode, som vi ofte skriver os selv i Java-applikationer, såsom getters, setters og toString (), for at nævne nogle få.
en anden af dens kommentarer er @NonNull. Så hvis et projekt allerede bruger Lombok til at eliminere kedelpladekode, kan @NonNull erstatte behovet for null-kontrol.
før vi går videre for at se nogle eksempler, lad os tilføje en Maven-afhængighed for Lombok:
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.6</version></dependency>
nu kan vi bruge @NonNull, hvor en null-kontrol er nødvendig:
public void accept(@NonNull Object param){ System.out.println(param);}
så vi kommenterede simpelthen det objekt, som null-kontrollen ville have været påkrævet for, og Lombok genererer den kompilerede klasse:
public void accept(@NonNull Object param) { if (param == null) { throw new NullPointerException("param"); } else { System.out.println(param); }}
Hvis Param er null, kaster denne metode en nullpointerundtagelse. Metoden skal gøre dette eksplicit i sin kontrakt, og klientkoden skal håndtere undtagelsen.
9, 2. Brug af StringUtils
generelt inkluderer Strengvalidering en check for en tom værdi ud over null-værdi. Derfor ville en fælles valideringserklæring være:
public void accept(String param){ if (null != param && !param.isEmpty()) System.out.println(param);}
dette bliver hurtigt overflødigt, hvis vi skal håndtere mange Strengtyper. Det er her StringUtils kommer praktisk. Før vi ser dette i aktion, lad os tilføje en Maven afhængighed for commons-lang3:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.11</version></dependency>
lad os nu refactor ovenstående kode med StringUtils:
public void accept(String param) { if (StringUtils.isNotEmpty(param)) System.out.println(param);}
så vi erstattede vores null eller tomme check med en statisk hjælpemetode isNotEmpty(). Denne API tilbyder andre kraftfulde værktøjsmetoder til håndtering af almindelige strengfunktioner.
konklusion
i denne artikel kiggede vi på de forskellige grunde til Nullpointerundtagelse og hvorfor det er svært at identificere. Derefter så vi forskellige måder at undgå redundansen i kode omkring kontrol af null med parametre, returtyper og andre variabler.
alle eksemplerne er tilgængelige på GitHub.
kom i gang med Spring 5 og Spring Boot 2, gennem Lær Spring course:
>> tjek kurset