vermijd controle op Null Statement in Java

overzicht

over het algemeen zijn null variabelen, referenties en collecties lastig te hanteren in Java-code. Niet alleen zijn ze moeilijk te identificeren, maar ze zijn ook complex om mee om te gaan.

in feite kan elke misser in het omgaan met null niet worden geïdentificeerd tijdens het compileren en resulteert in een Nullpointerexceptie tijdens runtime.

In deze tutorial zullen we kijken naar de noodzaak om te controleren op null in Java en verschillende alternatieven die ons helpen om null controles in onze code te voorkomen.

Further reading:

Using NullAway to Avoid NullPointerExceptions

Learn how to avoid NullPointerExceptions using NullAway.
Read more →

Spring Null-Safety Annotations

A quick and practical guide to null-safety annotations in Spring.
Read more →

Introduction to the Null Object Pattern

Learn about the Null Object Pattern and how to implement it in Java
Read more →

What Is NullPointerException?

Volgens de Javadoc voor NullPointerException, het is geworpen wanneer een toepassing probeert te gebruiken null in een geval waarin een object nodig, zoals:

  • het Aanroepen van een exemplaar methode van een null-object
  • Toegang tot of het wijzigen van een veld een null-object
  • het Nemen van de lengte van null als een array
  • Toegang tot of het wijzigen van de slots van null als een array
  • het Gooien van null als een Throwable waarde

Laten we snel zie je een paar voorbeelden van de Java-code die ervoor zorgen dat deze uitzondering:

public void doSomething() { String result = doSomethingElse(); if (result.equalsIgnoreCase("Success")) // success }}private String doSomethingElse() { return null;}

hier probeerden we een methode aan te roepen voor een null referentie. Dit zou resulteren in een Nulpuntuitzondering.

een ander algemeen voorbeeld is als we een null array proberen te benaderen:

public static void main(String args) { findMax(null);}private static void findMax(int arr) { int max = arr; //check other elements in loop}

Dit veroorzaakt een Nulpuntexceptie op Regel 6.

toegang tot een veld, Methode of index van een null-object veroorzaakt dus een Nulpuntexceptie, zoals blijkt uit de voorbeelden hierboven.

een veel voorkomende manier om de Nullpointerexceptie te vermijden is te controleren op null:

public void doSomething() { String result = doSomethingElse(); if (result != null && result.equalsIgnoreCase("Success")) { // success } else // failure}private String doSomethingElse() { return null;}

in de echte wereld vinden programmeurs het moeilijk om te bepalen welke objecten null kunnen zijn. Een agressief veilige strategie zou kunnen zijn om null te controleren voor elk object. Dit veroorzaakt echter veel overbodige nulcontroles en maakt onze code minder leesbaar.

In de volgende paragrafen zullen we enkele van de alternatieven in Java doornemen die dergelijke redundantie vermijden.

null hanteren via het API-Contract

zoals besproken in de laatste sectie, veroorzaakt toegang tot methoden of variabelen Van null-objecten een Nullpointerexceptie. We bespraken ook dat het plaatsen van een nulcontrole op een object voordat het wordt geopend, de mogelijkheid van Nulpuntexceptie elimineert.

echter, vaak zijn er API ‘ s die null waarden kunnen verwerken. Bijvoorbeeld:

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; }}

de print() methode aanroep zou gewoon” null ” afdrukken, maar zal geen uitzondering geven. Op dezelfde manier zou process() nooit null retourneren in zijn antwoord. Het werpt eerder een uitzondering.

dus voor een client code die toegang heeft tot de bovenstaande API ‘ s, is er geen null check nodig.

echter, dergelijke API ‘ s moeten het expliciet in hun contract. Een gemeenschappelijke plaats voor API ‘ s om een dergelijk contract te publiceren is de JavaDoc.

Dit geeft echter geen duidelijke indicatie van het API-contract en vertrouwt dus op de ontwikkelaars van de client code om de naleving ervan te garanderen.

in de volgende sectie zullen we zien hoe een paar IDEs en andere ontwikkeltools ontwikkelaars hiermee helpen.

automatiseren van API-contracten

4.1. Het gebruik van statische codeanalyse

statische codeanalysetools helpt de codekwaliteit aanzienlijk te verbeteren. En een paar van dergelijke tools kunnen ook de ontwikkelaars om de null contract te behouden. Een voorbeeld is FindBugs.

FindBugs helpt bij het beheren van het null contract door de annotaties @Nullable en @NonNull. We kunnen deze annotaties gebruiken over elke methode, veld, lokale variabele of parameter. Dit maakt het expliciet voor de client code of het geannoteerde type null kan zijn of niet. Laten we een voorbeeld bekijken:

public void accept(@Nonnull Object param) { System.out.println(param.toString());}

Hier maakt @NonNull duidelijk dat het argument niet null kan zijn. Als de client code deze methode aanroept zonder het argument voor null te controleren, zal FindBugs een waarschuwing genereren tijdens het compileren.

4.2. Met behulp van IDE-ondersteuning

vertrouwen ontwikkelaars over het algemeen op IDEs voor het schrijven van Java-code. En functies zoals smart code completion en nuttige waarschuwingen, zoals wanneer een variabele misschien niet zijn toegewezen, zeker helpen voor een groot deel.

sommige IDEs stellen ontwikkelaars ook in staat om API-contracten te beheren en zo de noodzaak voor een statische code analyse tool te elimineren. IntelliJ IDEA biedt de @NonNull en @ Nullable annotaties. Om de ondersteuning voor deze annotaties in IntelliJ toe te voegen, moeten we de volgende Maven-afhankelijkheid toevoegen:

<dependency> <groupId>org.jetbrains</groupId> <artifactId>annotations</artifactId> <version>16.0.2</version></dependency>

nu zal IntelliJ een waarschuwing genereren als De null check ontbreekt, zoals in ons laatste voorbeeld.

IntelliJ biedt ook een Contractannotatie voor het verwerken van complexe API-contracten.

5. Beweringen

tot nu toe hebben we alleen gesproken over het verwijderen van de noodzaak van nul controles uit de client code. Maar, dat is zelden van toepassing in real-world toepassingen.

stel nu dat we werken met een API die geen null parameters kan accepteren of een null-antwoord kan retourneren dat door de client moet worden afgehandeld. Dit toont de noodzaak voor ons om de parameters of de respons op een null waarde te controleren.

Hier kunnen we Java Assertions gebruiken in plaats van de traditionele null check conditional statement:

public void accept(Object param){ assert param != null; doSomething(param);}

in regel 2 controleren we op een null parameter. Als de beweringen zijn ingeschakeld, zou dit resulteren in een AssertionError.

hoewel het een goede manier is om pre-voorwaarden zoals Niet-null parameters te stellen, heeft deze aanpak twee grote problemen:

  1. beweringen zijn meestal uitgeschakeld in een JVM
  2. een valse bewering resulteert in een niet-aangevinkte fout die oninbaar is

daarom wordt het niet aanbevolen voor programmeurs om beweringen te gebruiken voor het controleren van voorwaarden. In de volgende paragrafen zullen we andere manieren bespreken om null validaties te behandelen.

vermijden van Nulcontroles door Coderingspraktijken

6.1. Randvoorwaarden

Het is meestal een goede gewoonte om code te schrijven die vroegtijdig mislukt. Daarom, als een API accepteert meerdere parameters die niet mogen worden null, het is beter om te controleren voor elke Niet-null parameter als voorwaarde voor de API.

bijvoorbeeld, laten we eens kijken naar twee methoden – een die vroeg faalt, en een die dat niet doet:

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); }}

Het is duidelijk dat we goodAccept() boven badAccept () moeten verkiezen.

als alternatief kunnen we ook Guava ‘ s randvoorwaarden gebruiken voor het valideren van API parameters.

6.2. Het gebruik van Primitieven in plaats van Wrapper klassen

aangezien null geen aanvaardbare waarde is voor primitieven zoals int, zouden we ze waar mogelijk moeten verkiezen boven hun wrapper tegenhangers zoals Integer.

beschouw twee implementaties van een methode die twee gehele getallen optelt:

public static int primitiveSum(int a, int b) { return a + b;}public static Integer wrapperSum(Integer a, Integer b) { return a + b;}

laten we nu deze API ‘ s in onze client code aanroepen:

int sum = primitiveSum(null, 2);

Dit zou resulteren in een compile-time fout aangezien null geen geldige waarde is voor een int.

en bij het gebruik van de API met wrapper klassen, krijgen we een Nullpointerexceptie:

assertThrows(NullPointerException.class, () -> wrapperSum(null, 2));

Er zijn ook andere factoren voor het gebruik van primitieven over wrappers, zoals we behandeld hebben in een andere tutorial, Java Primitieven versus objecten.

6.3. Lege Verzamelingen

soms moeten we een verzameling retourneren als antwoord van een methode. Voor dergelijke methoden moeten we altijd proberen om een lege verzameling te retourneren in plaats van null:

public List<String> names() { if (userExists()) { return Stream.of(readName()).collect(Collectors.toList()); } else { return Collections.emptyList(); }}

daarom hebben we de noodzaak voor onze client vermeden om een null check uit te voeren bij het aanroepen van deze methode.

met behulp van objecten

Java 7 introduceerde de nieuwe objecten API. Deze API heeft verschillende statische hulpprogramma methoden die weg te nemen veel redundante code. Laten we eens kijken naar een dergelijke methode, requireNonNull():

public void accept(Object param) { Objects.requireNonNull(param); // doSomething()}

nu, laten we testen de accept() methode:

assertThrows(NullPointerException.class, () -> accept(null));

dus, als null wordt doorgegeven als een argument, accept() gooit een Nullpointerexceptie.

Deze klasse heeft ook IsNull () en nonNull () methoden die gebruikt kunnen worden als predicaten om een object op null te controleren.

met behulp van Optioneel

8.1. Het gebruik van orElseThrow

Java 8 introduceerde een nieuwe optionele API in de taal. Dit biedt een beter contract voor het verwerken van optionele waarden in vergelijking met null. Laten we eens kijken hoe optioneel de noodzaak van nulcontroles wegneemt:

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; }}

door een optioneel terug te sturen, zoals hierboven getoond, maakt de procesmethode het de beller duidelijk dat het antwoord leeg kan zijn en tijdens het compileren moet worden afgehandeld.

Dit neemt met name de noodzaak weg Voor null controles in de client code. Een leeg antwoord kan anders worden behandeld met behulp van de declaratieve stijl van de optionele API:

assertThrows(Exception.class, () -> process(false).orElseThrow(() -> new Exception()));

Verder biedt het ook een beter contract voor API-ontwikkelaars om aan de clients aan te geven dat een API een leeg antwoord kan geven.

hoewel we de noodzaak van een null check op de beller van deze API elimineerden, gebruikten we het om een leeg antwoord terug te geven. Om dit te voorkomen, biedt Optional een ofNullable methode die een optioneel retourneert met de opgegeven waarde, of leeg, als de waarde null is:

public Optional<Object> process(boolean processed) { String response = doSomething(processed); return Optional.ofNullable(response);}

8.2. Optioneel gebruiken bij collecties

bij lege collecties is optioneel handig:

public String findFirst() { return getList().stream() .findFirst() .orElse(DEFAULT_VALUE);}

Deze functie wordt verondersteld het eerste item van een lijst terug te geven. De Findfirst-functie van de Stream API retourneert een lege optionele wanneer er geen gegevens. Hier hebben we orElse gebruikt om in plaats daarvan een standaardwaarde op te geven.

Dit staat ons toe om lege lijsten af te handelen, of lijsten, die, nadat we de filtermethode van de Stream library hebben gebruikt, geen items hebben om te leveren.

als alternatief kunnen we de client ook toestaan om te beslissen hoe leeg te verwerken door Optioneel van deze methode te retourneren:

public Optional<String> findOptionalFirst() { return getList().stream() .findFirst();}

daarom, als het resultaat van getList leeg is, zal deze methode een lege optie naar de client retourneren.

het gebruik van Optioneel met Collecties stelt ons in staat API ‘ s te ontwerpen die zeker niet-null waarden zullen retourneren, waardoor expliciete nulcontroles op de client worden vermeden.

Het is belangrijk om hier op te merken dat deze implementatie afhankelijk is van getList die geen null retourneert. Echter, zoals we besproken in de laatste sectie, Het is vaak beter om een lege lijst in plaats van een null retourneren.

8.3. Combining Optionals

wanneer we beginnen met het maken van onze functies retourneren optioneel hebben we een manier nodig om hun resultaten te combineren in een enkele waarde. Laten we onze getList voorbeeld van eerder. Wat als het een optionele lijst zou retourneren, of zou worden omwikkeld met een methode die een null omwikkelde met optioneel gebruik van ofNullable?

onze findFirst methode wil een optioneel eerste element van een optionele lijst retourneren:

public Optional<String> optionalListFirst() { return getOptionalList() .flatMap(list -> list.stream().findFirst());}

door gebruik te maken van de flatMap functie op de optionele geretourneerd van getOptional kunnen we het resultaat uitpakken van een innerlijke expressie die optioneel retourneert. Zonder flatMap zou het resultaat optioneel zijn<optioneel<String>>>. De flatMap-bewerking wordt alleen uitgevoerd als de optionele niet leeg is.

bibliotheken

9.1. Het gebruik van Lombok

Lombok is een geweldige bibliotheek die de hoeveelheid boilerplate code in onze projecten vermindert. Het wordt geleverd met een reeks annotaties die de plaats innemen van gemeenschappelijke delen van code die we vaak zelf schrijven in Java-toepassingen, zoals getters, setters, en toString(), om er een paar te noemen.

een andere van de annotaties is @NonNull. Dus, als een project al Lombok gebruikt om boilerplate code te elimineren, kan @NonNull de behoefte aan nul controles vervangen.

voordat we verder gaan om enkele voorbeelden te zien, laten we een Maven-afhankelijkheid toevoegen voor Lombok:

<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.6</version></dependency>

nu kunnen we @NonNull gebruiken waar een null-controle nodig is:

public void accept(@NonNull Object param){ System.out.println(param);}

dus, we annoteerden gewoon het object waarvoor de null-controle nodig zou zijn geweest, en Lombok genereert de gecompileerde klasse:

public void accept(@NonNull Object param) { if (param == null) { throw new NullPointerException("param"); } else { System.out.println(param); }}

als param null is, gooit deze methode een nulpuntuitzondering. De methode moet dit expliciet maken in haar contract, en de client code moet de uitzondering behandelen.

9.2. Met behulp van StringUtils

in het algemeen omvat de Stringvalidatie een controle op een lege waarde naast de nulwaarde. Daarom zou een veelgebruikte validatieverklaring zijn:

public void accept(String param){ if (null != param && !param.isEmpty()) System.out.println(param);}

Dit wordt al snel overbodig als we te maken hebben met veel Stringtypes. Hier komt StringUtils van pas. Voordat we dit in actie zien, laten we een Maven afhankelijkheid toevoegen voor commons-lang3:

<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.11</version></dependency>

laten we nu de bovenstaande code refactor met StringUtils:

public void accept(String param) { if (StringUtils.isNotEmpty(param)) System.out.println(param);}

dus hebben we onze null of lege controle vervangen door een statische utility methode isNotEmpty(). Deze API biedt andere krachtige hulpprogramma methoden voor het omgaan met gemeenschappelijke string functies.

conclusie

in dit artikel hebben we gekeken naar de verschillende redenen voor Nullpointerexceptie en waarom het moeilijk te identificeren is. Vervolgens zagen we verschillende manieren om de redundantie in code te voorkomen rond het controleren op null met parameters, retourtypen en andere variabelen.

alle voorbeelden zijn beschikbaar op GitHub.

aan de slag met Spring 5 en Spring Boot 2, Via de learn Spring course:

>> bekijk de cursus



Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.