Vältä tyhjän lausekkeen tarkistamista Javassa
yleiskatsaus
yleisesti, nollamuuttujat, viittaukset ja kokoelmat ovat hankalia käsitellä Java-koodissa. Niitä on vaikea tunnistaa, mutta ne ovat myös monimutkaisia käsitellä.
itse asiassa mitään puuttumista null: n käsittelyssä ei voida tunnistaa käännösajankohtana, ja tuloksena on Nullpointerception suorituksen aikana.
tässä opetusohjelmassa tarkastelemme tarvetta tarkistaa null Java-kielellä ja erilaisia vaihtoehtoja, joiden avulla voimme välttää null-tarkistukset koodissamme.
Further reading:
Using NullAway to Avoid NullPointerExceptions
Spring Null-Safety Annotations
Introduction to the Null Object Pattern
What Is NullPointerException?
Nullpointerekception javadocin mukaan se heitetään, kun sovellus yrittää käyttää nullia tapauksessa, jossa objektia tarvitaan, kuten:
- kutsuu null-objektin instanssimenetelmää
- käyttäen tai muokkaamalla nollan objektin kenttää
- ottaen nollin pituuden ikään kuin se olisi array
- käyttäen tai muuttaen nollin aukkoja ikään kuin se olisi array
- heittää Nollaa ikään kuin se olisi heittoarvo
katsotaan nopeasti muutama esimerkki Java-koodista, jotka aiheuttavat tämän poikkeuksen:
public void doSomething() { String result = doSomethingElse(); if (result.equalsIgnoreCase("Success")) // success }}private String doSomethingElse() { return null;}
tässä yritimme vedota metodikutsuun nollaviitteestä. Tämä johtaisi nollatulokseen.
toinen yleinen esimerkki on, jos yritämme käyttää nollajoukkoa:
public static void main(String args) { findMax(null);}private static void findMax(int arr) { int max = arr; //check other elements in loop}
Tämä aiheuttaa Nollapoikkeaman linjalla 6.
näin ollen minkään nollakohdan kentän, menetelmän tai indeksin käyttäminen aiheuttaa Nollapointerception, kuten yllä olevista esimerkeistä voidaan nähdä.
yleinen tapa välttää Nullpointerception on tarkistaa null:
public void doSomething() { String result = doSomethingElse(); if (result != null && result.equalsIgnoreCase("Success")) { // success } else // failure}private String doSomethingElse() { return null;}
reaalimaailmassa ohjelmoijien on vaikea tunnistaa, mitkä oliot voivat olla nollia. Aggressiivisesti turvallinen strategia voisi olla tarkistaa null jokaisen esineen. Tämä kuitenkin aiheuttaa paljon tarpeettomia nollatarkistuksia ja tekee koodistamme vähemmän luettavaa.
seuraavissa jaksoissa käydään läpi joitakin Javan vaihtoehtoja, joilla vältetään tällainen redundanssi.
null: n käsittely API-sopimuksen kautta
kuten viimeisessä jaksossa on käsitelty, null-objektien menetelmien tai muuttujien käyttö aiheuttaa Nullpointerception. Keskustelimme myös siitä, että nollatarkastuksen tekeminen esineelle ennen sen käyttöä poistaa NullPointerException mahdollisuuden.
usein on kuitenkin olemassa sovellusliittymiä, jotka pystyvät käsittelemään nollan arvoja. Esimerkiksi:
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; }}
print () – menetelmäkutsu vain tulostaisi ”null”, mutta ei heitä poikkeusta. Vastaavasti prosessi () ei koskaan palauttaisi nollia vastauksessaan. Se tekee poikkeuksen.
joten yllä oleviin sovellusliittymiin pääsevälle asiakaskoodille ei tarvitse tehdä nollatarkistusta.
tällaisten sovellusliittymien on kuitenkin ilmoitettava siitä sopimuksessaan. Javadoc on yleinen paikka Sovellusliittymille tällaisen sopimuksen julkaisemiseksi.
tämä ei kuitenkaan anna selkeää kuvaa API-sopimuksesta, joten asiakaskoodin kehittäjät varmistavat sen noudattamisen.
seuraavassa jaksossa nähdään, miten muutama IDE ja muut kehitystyökalut auttavat kehittäjiä tässä.
API-sopimusten automatisointi
4.1. Staattisen koodin analysointi
staattisen koodin analysointityökalut auttavat parantamaan koodin laatua paljon. Ja muutama tällainen työkalu mahdollistaa myös Kehittäjät säilyttää null sopimuksen. Yksi esimerkki on FindBugs.
FindBugs auttaa hallinnoimaan nollasopimusta @Nullable-ja @nonnull-merkintöjen kautta. Voimme käyttää näitä merkintöjä millä tahansa menetelmällä, kentällä, paikallisella muuttujalla tai parametrilla. Tämä tekee asiakaskoodille selväksi, voiko merkintätyyppi olla nolla vai ei. Katsotaanpa esimerkki:
public void accept(@Nonnull Object param) { System.out.println(param.toString());}
täällä @NonNull tekee selväksi, ettei argumentti voi olla nolla. Jos asiakaskoodi kutsuu tätä menetelmää tarkistamatta argumenttia null, FindBugs tuottaisi varoituksen käännösaikaan.
4, 2. IDE-tuen
kehittäjät käyttävät yleensä IDE: tä Java-koodin kirjoittamiseen. Ja ominaisuuksia, kuten älykäs koodin täydennys ja hyödyllisiä varoituksia, kuten kun muuttuja ei ehkä ole määritetty, varmasti auttaa paljon.
jotkut IDE: t antavat myös kehittäjille mahdollisuuden hallita API-sopimuksia ja siten poistaa staattisen koodin analysointityökalun tarpeen. IntelliJ IDEA tarjoaa @NonNull ja @ Nullable merkinnät. Lisätäksemme tuen näille merkinnöille Intellijissä, meidän on lisättävä seuraava Maven-riippuvuus:
<dependency> <groupId>org.jetbrains</groupId> <artifactId>annotations</artifactId> <version>16.0.2</version></dependency>
nyt IntelliJ luo varoituksen, jos nollatarkistus puuttuu, kuten edellisessä esimerkissämme.
IntelliJ tarjoaa myös Sopimushuomautuksen monimutkaisten API-sopimusten käsittelyyn.
5.
tähän asti olemme puhuneet vain nollatarkistusten tarpeen poistamisesta asiakaskoodista. Mutta, että on harvoin sovellettavissa reaalimaailman sovelluksia.
nyt oletetaan, että työskentelemme API: n kanssa, joka ei voi hyväksyä nollaparametreja tai voi palauttaa nollavasteen, joka asiakkaan on käsiteltävä. Tämä edellyttää, että tarkistamme parametrit tai vastauksen nollan arvon osalta.
tässä voidaan käyttää Java-väitettä perinteisen null check-ehdollisen lausekkeen sijaan:
public void accept(Object param){ assert param != null; doSomething(param);}
rivillä 2 tarkistamme null-parametrin. Jos väitteet ovat käytössä, tämä johtaisi Väittämävirheeseen.
vaikka se on hyvä tapa esittää ennakkoehtoja, kuten ei-nollaparametreja, tässä lähestymistavassa on kaksi suurta ongelmaa:
- väitteet on yleensä poistettu JVM: ssä
- virheellinen väite johtaa tarkistamattomaan virheeseen, jota ei voi periä
siksi ohjelmoijien ei suositella käyttävän väittämiä olosuhteiden tarkistamiseen. Seuraavissa jaksoissa keskustellaan muista tavoista käsitellä null validations.
Nollatarkastusten välttäminen Koodauskäytännöillä
6.1. Ennakkoedellytykset
on yleensä hyvä tapa kirjoittaa koodia, joka epäonnistuu aikaisin. Siksi, jos API hyväksyy useita parametreja, jotka eivät saa olla null, on parempi tarkistaa jokainen ei-null parametri API: n edellytyksenä.
tarkastellaan esimerkiksi kahta menetelmää – yhtä, joka epäonnistuu varhain, ja yhtä, joka ei:
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); }}
selvästi, suositaan goodacceptiä() badacceptin () sijasta.
vaihtoehtona Voimme käyttää myös guavan reunaehtoja API-parametrien validoinnissa.
6, 2. Käyttämällä primitiivejä Kääreluokkien
sijaan, koska null ei ole hyväksyttävä arvo INT: n kaltaisille primitiiveille, meidän tulisi suosia niitä mahdollisuuksien mukaan niiden Kääreluokkien, kuten kokonaisluvun, sijasta.
tarkastellaan kahta toteutusta menetelmästä, joka summaa kaksi kokonaislukua:
public static int primitiveSum(int a, int b) { return a + b;}public static Integer wrapperSum(Integer a, Integer b) { return a + b;}
nyt kutsutaan näitä ohjelmointirajapintoja asiakastunnuksessamme:
int sum = primitiveSum(null, 2);
tämä johtaisi käännösaikavirheeseen, koska null ei ole kelvollinen arvo int: lle.
ja käytettäessä API: a kääreluokilla, saamme NullPointerException:
assertThrows(NullPointerException.class, () -> wrapperSum(null, 2));
primitiivien käyttämiseen kääreiden päällä on myös muita tekijöitä, kuten toisessa tutoriaalissa, Java Primitives vs. Objects.
6, 3. Tyhjät kokoelmat
joskus joudumme palauttamaan kokoelman vastauksena menetelmästä. Tällaisia menetelmiä varten meidän tulisi aina yrittää palauttaa tyhjä kokoelma nollan sijaan:
public List<String> names() { if (userExists()) { return Stream.of(readName()).collect(Collectors.toList()); } else { return Collections.emptyList(); }}
näin ollen olemme välttäneet asiakkaamme tarvetta tehdä nollatarkistus tätä menetelmää kutsuessamme.
käyttäen olioita
Java 7 esitteli uuden olioiden API: n. Tämä API on useita staattisia apuohjelma menetelmiä, jotka vievät paljon tarpeetonta koodia. Tarkastellaan yhtä tällaista menetelmää, requirenonnullia ():
public void accept(Object param) { Objects.requireNonNull(param); // doSomething()}
nyt testataan accept () – menetelmää:
assertThrows(NullPointerException.class, () -> accept(null));
joten, jos nolla hyväksytään argumenttina, accept () heittää Nullpointerception.
tässä luokassa on myös isNull () – ja nonNull () – menetelmiä, joita voidaan käyttää predikaatteina objektin tarkistamiseksi nollille.
käyttäen valinnaista
8.1. Käyttämällä orElseThrow
Java 8 esitteli kielelle uuden valinnaisen API: n. Tämä tarjoaa paremman sopimuksen valinnaisten arvojen käsittelyyn kuin null. Katsotaan, miten valinnaisuus vie tyhjätarkistusten tarpeen:
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; }}
palauttamalla valinnaisen, kuten yllä on esitetty, prosessimenetelmä tekee soittajalle selväksi, että vastaus voi olla tyhjä ja se on käsiteltävä käännösaikaan.
Tämä erityisesti poistaa tarpeen tehdä nollatarkistuksia asiakaskoodissa. Tyhjää vastausta voidaan käsitellä eri tavalla valinnaisen API: n deklaratiivisella tyylillä:
assertThrows(Exception.class, () -> process(false).orElseThrow(() -> new Exception()));
lisäksi se tarjoaa API-kehittäjille paremman sopimuksen viestittää asiakkaille, että API voi palauttaa tyhjän vastauksen.
vaikka poistimme tarpeen tarkistaa tämän API: n soittaja, käytimme sitä palauttaaksemme tyhjän vastauksen. Tämän välttämiseksi valinnainen tarjoaa ofNullable-menetelmän, jossa valinnainen palautetaan annetulla arvolla tai tyhjänä, jos arvo on nolla:
public Optional<Object> process(boolean processed) { String response = doSomething(processed); return Optional.ofNullable(response);}
8.2. Valinnaisen käyttäminen kokoelmien kanssa
käsiteltäessä tyhjiä kokoelmia, valinnainen on kätevä:
public String findFirst() { return getList().stream() .findFirst() .orElse(DEFAULT_VALUE);}
tämän funktion on tarkoitus palauttaa luettelon ensimmäinen kohta. Stream API: n findFirst-toiminto palauttaa tyhjän valinnaisen, kun Tietoja ei ole. Tässä, olemme käyttäneet orElse antaa oletusarvo sijaan.
näin voidaan käsitellä joko tyhjiä listoja tai listoja, joihin Streamikirjaston suodatusmenetelmän käytön jälkeen ei ole toimitettavia kohteita.
vaihtoehtoisesti voimme myös antaa asiakkaan päättää tyhjän käsittelystä palaamalla valinnaisena tähän menetelmään:
public Optional<String> findOptionalFirst() { return getList().stream() .findFirst();}
näin ollen, jos getlistan tulos on tyhjä, tämä menetelmä palauttaa tyhjän valinnaisen ominaisuuden asiakkaalle.
käyttämällä valinnaisia kokoelmia voimme suunnitella ohjelmointirajapintoja, jotka palauttavat varmasti ei-null-arvot, jolloin vältetään asiakkaan eksplisiittiset nollatarkistukset.
on tärkeää huomata tässä, että tämä toteutus perustuu siihen, että getList ei palauta null: ää. Kuitenkin, kuten keskustelimme viimeisessä osassa, se on usein parempi palauttaa tyhjä lista sijaan nolla.
8, 3. Vaihtoehtojen yhdistäminen
kun alamme tehdä funktioistamme Paluuvalmiita, tarvitsemme tavan yhdistää niiden tulokset yhdeksi arvoksi. Otetaan meidän getList esimerkki aiemmasta. Mitä jos se olisi palauttaa valinnaisen luettelon, tai olisi kääritty menetelmällä, joka käärittiin null valinnainen käyttämällä ofNullable?
findFirst-menetelmämme haluaa palauttaa valinnaisen luettelon ensimmäisen elementin:
public Optional<String> optionalListFirst() { return getOptionalList() .flatMap(list -> list.stream().findFirst());}
käyttämällä Getoptionalilta palautetun valinnaisen flatMap-funktion avulla voimme purkaa sisäisen lausekkeen tuloksen, joka palauttaa valinnaisen. Ilman flatmapia tulos olisi valinnainen<valinnainen<String>>. FlatMap-toiminto suoritetaan vain, kun Valinnainen toiminto ei ole tyhjä.
kirjastot
9.1. Lombok
Lombok on loistava kirjasto, joka vähentää boilerplate-koodin määrää projekteissamme. Sen mukana tulee joukko merkintöjä, jotka korvaavat yhteiset koodin osat, joita usein kirjoitamme itse Java-sovelluksissa, kuten getterit, setterit ja toString(), muutamia mainitaksemme.
toinen sen merkinnöistä on @NonNull. Joten, jos projekti käyttää jo Lombok poistaa boilerplate koodi, @NonNull voi korvata tarpeen null tarkastuksia.
ennen kuin siirrymme katsomaan joitakin esimerkkejä, lisätään Maven-riippuvuus Lombokille:
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.6</version></dependency>
nyt voimme käyttää @Nonnullia missä tahansa tarvitaan null check:
public void accept(@NonNull Object param){ System.out.println(param);}
niin, me yksinkertaisesti huomautimme kohteen, jolle null check olisi vaadittu, ja Lombok tuottaa kootun luokan:
public void accept(@NonNull Object param) { if (param == null) { throw new NullPointerException("param"); } else { System.out.println(param); }}
jos param on nolla, tämä menetelmä heittää nullpointerception. Menetelmän on mainittava tämä sopimuksessaan, ja asiakaskoodin on käsiteltävä poikkeusta.
9, 2. Käyttämällä stringutils
yleensä merkkijonon validointi sisältää tyhjän arvon tarkistamisen nollan arvon lisäksi. Siksi yleinen validointilauseke olisi:
public void accept(String param){ if (null != param && !param.isEmpty()) System.out.println(param);}
Tämä muuttuu nopeasti tarpeettomaksi, jos joudumme käsittelemään paljon Merkkijonotyyppejä. Tässä StringUtils on kätevä. Ennen kuin näemme tämän toiminnassa, lisätään Maven-riippuvuus commons-lang3:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.11</version></dependency>
:
public void accept(String param) { if (StringUtils.isNotEmpty(param)) System.out.println(param);}
joten korvasimme tyhjän tai tyhjän tarkistuksemme staattisella hyödyllisyysmenetelmällä isNotEmpty(). Tämä API tarjoaa muita tehokkaita hyödyllisyysmenetelmiä tavallisten Merkkijonotoimintojen käsittelyyn.
johtopäätös
tässä jutussa tarkastelimme erilaisia syitä Nollapoikkeamaan ja miksi sitä on vaikea tunnistaa. Sitten, näimme eri tapoja välttää irtisanominen koodi noin tarkistaa null parametrit, palata tyypit, ja muut muuttujat.
kaikki esimerkit löytyvät GitHubista.
Aloita kevät 5: llä ja Kevätkävely 2: lla Oppimiskurssin kautta:
>> tsekkaa kurssi