Vyhněte se kontrole nulového příkazu v Javě

přehled

obecně platí, že nulové proměnné, odkazy a sbírky jsou v kódu Java složité. Nejen, že je těžké je identifikovat, ale je také složité se s nimi vypořádat.

ve skutečnosti nelze žádnou chybu při řešení null identifikovat v době kompilace a za běhu je výsledkem NullPointerException.

v tomto tutoriálu se podíváme na potřebu zkontrolovat null v Javě a různé alternativy, které nám pomáhají vyhnout se nulovým kontrolám v našem kódu.

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?

Podle Javadoc pro NullPointerException, je vyvolána, když je aplikace pokusí použít null v případě, kdy objekt je nutné, jako je:

  • Volání metody instance null objektu
  • Přístup nebo úpravy pole null objekt
  • délka null, jako kdyby to bylo pole
  • Přístup nebo úpravy sloty null, jako kdyby to bylo pole
  • Házení null, jako kdyby to byla Throwable hodnota

Pojďme se rychle podívat na několik příkladů kódu v jazyce Java, že příčinou této výjimky:

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

zde jsme se pokusili vyvolat volání metody pro nulový odkaz. To by mělo za následek NullPointerException.

dalším běžným příkladem je pokus o přístup k nulovému poli:

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

to způsobí NullPointerException na řádku 6.

přístup k libovolnému poli, metodě nebo indexu nulového objektu tedy způsobí NullPointerException, jak je vidět z výše uvedených příkladů.

běžným způsobem, jak se vyhnout výjimce NullPointerException, je zkontrolovat null:

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

v reálném světě programátoři těžko identifikují, které objekty mohou být nulové. Agresivně bezpečnou strategií by mohlo být zkontrolovat null pro každý objekt. To však způsobuje mnoho redundantních nulových kontrol a činí náš kód méně čitelným.

v několika následujících sekcích projdeme některé z alternativ v Javě, které se takové redundanci vyhýbají.

manipulace s nullem prostřednictvím smlouvy API

jak je popsáno v poslední části, přístup k metodám nebo proměnným objektů null způsobuje NullPointerException. Také jsme diskutovali, že vložení nulové kontroly na objekt před přístupem k němu eliminuje možnost NullPointerException.

často však existují API, která zvládnou hodnoty null. Například:

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

volání metody print() by pouze vytisklo „null“, ale nevyhodí výjimku. Podobně process() nikdy nevrátí null ve své odpovědi. Je to spíše výjimka.

takže pro klientský kód přistupující k výše uvedeným API není potřeba null check.

taková rozhraní API však musí být ve své smlouvě výslovně uvedena. Společným místem pro API publikovat takovou smlouvu je JavaDoc.

to však neposkytuje jasný údaj o smlouvě API, a proto se spoléhá na vývojáře klientského kódu, aby zajistili jeho dodržování.

v další části uvidíme, jak několik IDE a dalších vývojových nástrojů pomáhá vývojářům s tím.

automatizace smluv API

4.1. Použití statické analýzy kódu

statické nástroje pro analýzu kódu pomáhají výrazně zlepšit kvalitu kódu. A několik takových nástrojů také umožňuje vývojářům udržovat nulovou smlouvu. Jedním z příkladů jsou FindBugs.

FindBugs pomáhá spravovat nulovou smlouvu prostřednictvím anotací @Nullable a @NonNull. Tyto anotace můžeme použít na jakoukoli metodu, pole, místní proměnnou nebo parametr. Díky tomu je pro klientský kód explicitní, zda anotovaný typ může být null nebo ne. Podívejme se na příklad:

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

zde @NonNull objasňuje, že argument nemůže být null. Pokud kód klienta volá tuto metodu bez kontroly argumentu null, FindBugs vygeneruje varování v době kompilace.

4.2. Použití IDE podpory

vývojáři obecně spoléhají na IDE pro psaní kódu Java. A funkce, jako je inteligentní dokončení kódu a užitečná varování, jako když proměnná nemusí být přiřazena, určitě do značné míry pomáhají.

některé IDE také umožňují vývojářům spravovat smlouvy API a tím eliminovat potřebu statického nástroje pro analýzu kódu. IntelliJ IDEA poskytuje @NonNull a @ Nullable anotace. Chcete-li přidat podporu pro tyto anotace v IntelliJ, musíme přidat následující závislost Maven:

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

nyní IntelliJ vygeneruje varování, pokud chybí kontrola null, jako v našem posledním příkladu.

IntelliJ také poskytuje anotaci smlouvy pro zpracování složitých smluv API.

5. Tvrzení

až dosud jsme hovořili pouze o odstranění potřeby nulových kontrol z klientského kódu. Ale to je zřídka použitelné v aplikacích v reálném světě.

nyní předpokládejme, že pracujeme s API, které nemůže přijmout nulové parametry nebo může vrátit nulovou odpověď, kterou musí klient zpracovat. To představuje potřebu, abychom zkontrolovali parametry nebo odpověď na nulovou hodnotu.

zde můžeme použít tvrzení Java namísto tradičního podmíněného příkazu null check:

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

v řádku 2 zkontrolujeme parametr null. Pokud jsou tvrzení povolena, mělo by to za následek Asertionerror.

ačkoli je to dobrý způsob, jak prosadit předběžné podmínky, jako jsou nenulové parametry, tento přístup má dva hlavní problémy:

  1. Tvrzení jsou obvykle postižené v JVM
  2. falešné tvrzení, výsledky v nekontrolované chyba, že je nedobytné

z tohoto důvodu, to není doporučeno pro programátory použít Tvrzení pro kontrolu podmínek. V následujících částech budeme diskutovat o dalších způsobech zpracování nulových validací.

vyhnout se nulovým kontrolám pomocí kódovacích postupů

6.1. Předpoklady

obvykle je dobré psát kód, který selže brzy. Pokud tedy API přijímá více parametrů, které nesmí být nulové, je lepší zkontrolovat každý nenulový parametr jako předpoklad API.

například, pojďme se podívat na dva způsoby – jeden, že selže dřív, a ten, který není:

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

Jasně, měli bychom raději goodAccept() nad badAccept().

jako alternativu můžeme také použít Guavovy předpoklady pro validaci parametrů API.

6.2. Pomocí Primitiv Místo Wrapper Tříd

Protože null není přijatelná hodnota pro primitivy jako int, měli bychom jim přednost před jejich obal protějšky jako celé Číslo, kdekoli je to možné.

Vezměme dvě implementace metody, která sumy dvou celých čísel:

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

Nyní, řekněme, že je tato rozhraní Api v našem kód klienta:

int sum = primitiveSum(null, 2);

To by mělo za následek chyby při kompilaci, protože null není platná hodnota pro int.

a při použití API s třídami wrapperu získáme NullPointerException:

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

existují i další faktory pro použití primitiv nad obaly, jak jsme se zabývali v jiném tutoriálu, Java Primitives versus Objects.

6.3. Prázdné kolekce

občas potřebujeme vrátit kolekci jako odpověď z metody. Pro tyto metody, měli bychom si vždy vrátí prázdnou kolekci namísto null:

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

Tím jsme se vyhnuli nutnosti pro našeho klienta provést kontrolu null při volání této metody.

pomocí objektů

Java 7 představil nové objekty API. Toto API má několik statických metod, které odstraňují spoustu redundantního kódu. Pojďme se podívat na jeden takový způsob, requireNonNull():

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

Nyní, řekněme, test accept() metoda:

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

Takže, pokud null je předán jako argument, accept() vyhodí NullPointerException.

tato třída má také metody isNull() a nonNull (), které lze použít jako predikáty pro kontrolu null objektu.

pomocí volitelného

8.1. Pomocí orElseThrow

Java 8 představil nové Volitelné API v jazyce. To nabízí lepší smlouvu pro zpracování volitelných hodnot ve srovnání s null. Pojďme se podívat, jak Optional bere potřeba null kontroly:

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

Po návratu Volitelné, jak je uvedeno výše, proces, metoda, dává jasně najevo, k volajícímu, že reakce může být prázdný a musí být řešeny v době kompilace.

to zejména odstraňuje potřebu nulových kontrol v klientském kódu. Prázdná odpověď může být zpracována odlišně pomocí deklarativního stylu volitelného API:

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

Kromě toho, to také poskytuje lepší smlouvu API vývojářům najevo klientům, že API může vrátit prázdnou odpověď.

přestože jsme eliminovali potřebu nulové kontroly volajícího tohoto API, použili jsme jej k vrácení prázdné odpovědi. Pokud je hodnota null:

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

8.2. Použití volitelného s kolekcemi

při práci s prázdnými kolekcemi se Volitelné hodí:

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

Tato funkce má vrátit první položku seznamu. Funkce Findfirst Stream API vrátí prázdnou volitelnou funkci, pokud nejsou k dispozici žádná data. Zde jsme použili orElse, abychom místo toho poskytli výchozí hodnotu.

to nám umožňuje zpracovat buď prázdné seznamy, nebo seznamy, které poté, co jsme použili metodu filtru knihovny streamu, nemají žádné položky k dodání.

alternativně můžeme klientovi také umožnit, aby se rozhodl, jak zacházet s prázdným vrácením volitelného z této metody:

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

Proto, pokud je výsledek getList je prázdná, tato metoda vrátí prázdný Volitelné klienta.

použití volitelných kolekcí nám umožňuje navrhnout API, která jistě vrátí nenulové hodnoty, čímž se vyhneme explicitním nulovým kontrolám klienta.

je důležité si uvědomit, že tato implementace spoléhá na getList, který nevrací null. Nicméně, jak jsme diskutovali v poslední části, je často lepší vrátit prázdný seznam spíše než null.

8.3. Kombinace volitelných voleb

když začneme vracet Naše funkce volitelné, potřebujeme způsob, jak spojit jejich výsledky do jedné hodnoty. Vezměme si náš příklad getlistu z dřívějška. Co kdyby to mělo vrátit volitelný seznam, nebo měly být zabaleny metodou, která zabalila null s volitelným použitím ofNullable?

Naše metoda findFirst chce vrátit Volitelný první prvek Volitelný seznam:

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

pomocí flatMap funkce na Volitelné vrátil z getOptional můžeme rozbalit výsledkem vnitřní výraz, který vrací Volitelné. Bez flatMap, výsledek by byl Volitelný<Volitelné<String>>. Operace flatMap se provádí pouze v případě, že Volitelné není prázdné.

knihovny

9.1. Použití Lombok

Lombok je skvělá knihovna, která snižuje množství kódu kotle v našich projektech. Je dodáván se sadou popisy, které se místo společné části kódu, které jsme často píší sami v Java aplikacích, jako je například getter, setter, a toString(), abychom jmenovali alespoň některé.

Další z jeho anotací je @NonNull. Pokud tedy projekt již používá Lombok k odstranění kódu boilerplate, @NonNull může nahradit potřebu nulových kontrol.

předtím, Než jsme se přesunout na některé příklady, pojďme přidat Maven závislost na Lombok:

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

Nyní, můžeme použít @NonNull všude tam, kde null zkontrolujte, zda je potřeba:

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

Tak jsme se prostě komentovaný objektu, pro který null check by bylo nutné, a Lombok generuje zkompilované třídy:

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

Pokud parametr je null, tato metoda vyhodí NullPointerException. Metoda to musí ve své smlouvě výslovně uvést a výjimku musí zpracovat kód klienta.

9.2. Použití StringUtils

validace řetězců obecně zahrnuje kontrolu prázdné hodnoty kromě hodnoty null. Proto, časté ověřování prohlášení by bylo:

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

To se rychle stává nadbytečnou, pokud se budeme muset vypořádat s mnoha Řetězec typy. To je místo, kde StringUtils přijde vhod. Než to uvidíme v akci, přidáme závislost Maven pro commons-lang3:

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

pojďme nyní refaktorovat výše uvedený kód pomocí StringUtils:

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

takže jsme nahradili naši nulovou nebo prázdnou kontrolu statickou užitkovou metodou isNotEmpty (). Toto API nabízí další výkonné nástroje pro manipulaci s běžnými řetězcovými funkcemi.

závěr

v tomto článku jsme se podívali na různé důvody pro NullPointerException a proč je těžké identifikovat. Pak jsme viděli různé způsoby, jak se vyhnout redundanci v kódu kolem kontroly null s parametry, typy návratů a dalšími proměnnými.

všechny příklady jsou k dispozici na Githubu.

začněte s Jarní 5 a na Jaře Boot 2, a to prostřednictvím Naučit Jarní kurz:

>>



Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.