Ohjelmointi

Tehokas Java NullPointerException -käsittely

Ei tarvitse paljon Java-kehityskokemusta oppia omakohtaisesti, mistä NullPointerException on. Itse asiassa yksi henkilö on korostanut tämän käsittelemistä, koska Java-kehittäjät tekevät ykkösvirheen. Kirjoitin aiemmin blogia String.value (Object) -toiminnon käytöstä vähentääksesi ei-toivottuja NullPointerExceptions-arvoja. On olemassa useita muita yksinkertaisia ​​tekniikoita, joita voidaan käyttää vähentämään tai poistamaan tämän yleisen RuntimeException -tyypin esiintymät, jotka ovat olleet kanssamme JDK 1.0: n jälkeen. Tämä blogiviesti kerää ja tiivistää joitain näistä tekniikoista suosituimpia.

Tarkista jokaisen kohteen tyhjä tila ennen käyttöä

Varmin tapa välttää NullPointerException on tarkistaa kaikki objektiviittaukset varmistaaksesi, että ne eivät ole tyhjiä, ennen kuin pääset johonkin objektin kentistä tai menetelmistä. Kuten seuraava esimerkki osoittaa, tämä on hyvin yksinkertainen tekniikka.

final String causStr = "merkkijonon lisääminen Dequeen, joka on asetettu nollaksi."; final String elementStr = "Fudd"; Deque deque = nolla; kokeile {deque.push (elementStr); loki ("Successful at" + causStr, System.out); } catch (NullPointerException nullPointer) {loki (causStr, nullPointer, System.out); } kokeile {if (deque == null) {deque = uusi LinkedList (); } deque.push (elementStr); loki ("Onnistunut" + causStr + ": ssä (tarkistamalla ensin tyhjennys ja suorittaen Deque-toteutuksen)", System.out); } catch (NullPointerException nullPointer) {loki (causStr, nullPointer, System.out); } 

Yllä olevassa koodissa käytetty Deque alustetaan tarkoituksella nollaksi esimerkin helpottamiseksi. Ensimmäisen koodi yrittää lohko ei tarkista nollaa ennen kuin yrität käyttää Deque-menetelmää. Toisen koodi yrittää - lohko tarkistaa nollan ja välittää Deque (LinkedList), jos se on tyhjä. Molempien esimerkkien tulos näyttää tältä:

VIRHE: NullPointerException tapahtui yritettäessä lisätä merkkijono Dequeen, joka on asetettu nollaksi. java.lang.NullPointerException INFO: Onnistunut lisäämällä merkkijono Dequeen, joka on asetettu nollaksi. (tarkistamalla ensin tyhjennys ja saattamalla Deque-toteutus käyttöön) 

Ylläolevassa lähdössä VIRHEEN seuraava viesti osoittaa, että a NullPointerException heitetään, kun menetelmän kutsua yritetään nollalle Deque. INFO: ta seuraava viesti yllä olevassa lähdössä osoittaa, että tarkistamalla Deque ensin nollalle ja sitten sen uuden toteutuksen luomiselle, kun se on nolla, poikkeusta vältettiin kokonaan.

Tätä lähestymistapaa käytetään usein, ja kuten edellä on esitetty, se voi olla erittäin hyödyllinen välttää ei-toivottuja (odottamattomia) NullPointerException tapauksia. Se ei kuitenkaan ole ilman kustannuksia. Tyhjyyden tarkistaminen ennen jokaisen objektin käyttämistä voi pahentaa koodia, olla tylsää kirjoittaa ja avaa enemmän tilaa lisäkoodin kehittämisen ja ylläpidon ongelmille. Tästä syystä on puhuttu Java-kielen tuen käyttöönotosta sisäänrakennetulle tyhjennystunnistukselle, näiden tarkistusten automaattiseen lisäämiseen alkuperäisen koodauksen jälkeen, tyhjiin turvallisiin tyyppeihin, näkökohtisuuntaisen ohjelmoinnin (AOP) käyttöön nollatarkistuksen lisäämiseksi tavukoodi ja muut nollatunnistustyökalut.

Groovy tarjoaa jo kätevän mekanismin potentiaalisesti tyhjien objektiviittausten käsittelemiseksi. Groovyn turvallinen navigointioperaattori (?.) palauttaa nollan a-heiton sijasta NullPointerException kun null-objektiviittausta käytetään.

Koska nollan tarkistaminen jokaiselle objektiviitteelle voi olla ikävystyttävää ja paisuttaa koodia, monet kehittäjät valitsevat järkevästi, mitkä objektit tarkistetaan nollaamiseksi. Tämä johtaa yleensä kaikkien mahdollisesti tuntemattomien kohteiden nollan tarkistamiseen. Ajatuksena on, että esineet voidaan tarkistaa paljaissa rajapinnoissa ja niiden voidaan olettaa olevan turvallisia ensimmäisen tarkastuksen jälkeen.

Tämä on tilanne, jossa kolmiosainen operaattori voi olla erityisen hyödyllinen. Sijasta

// haki BigDecimalin nimeltä someObject String returnString; if (someObject! = null) {returnString = someObject.toEngineeringString (); } else {returnString = ""; } 

kolmikertainen operaattori tukee tätä tiiviimpää syntaksia

// haki BigDecimalin nimeltä someObject final String returnString = (someObject! = null)? someObject.toEngineeringString (): ""; } 

Tarkista menetelmän argumentit tyhjäksi

Juuri keskusteltua tekniikkaa voidaan käyttää kaikissa esineissä. Kuten kyseisen tekniikan kuvauksessa todetaan, monet kehittäjät päättävät tarkistaa objektien tyhjästä vain, kun ne tulevat "epäluotettavista" lähteistä. Tämä tarkoittaa usein nolla-aseman testaamista menetelmissä, jotka altistuvat ulkoisille soittajille. Esimerkiksi tietyssä luokassa kehittäjä voi päättää tarkistaa, onko kaikissa välitetyissä objekteissa tyhjä julkinen menetelmiä, mutta ei tarkista, onko tyhjä yksityinen menetelmiä.

Seuraava koodi osoittaa tämän nollan tarkistuksen menetelmän syötössä. Se sisältää yhden menetelmän esittelymenetelmänä, joka kääntyy ympäri ja kutsuu kaksi menetelmää, välittäen kunkin menetelmän yhden nolla-argumentin. Yksi nolla-argumentin vastaanottavista menetelmistä tarkistaa kyseisen argumentin ensin nollaksi, mutta toinen vain olettaa, että välitetty parametri ei ole nolla.

 / ** * Liitä ennalta määritelty merkkijono annettuun StringBuilderiin. * * @param builder StringBuilder, johon on liitetty teksti; pitäisi olla * nolla. * @throws IllegalArgumentException heitetään, jos annettu StringBuilder on * tyhjä. * / private void appendPredefinedTextToProvidedBuilderCheckForNull (viimeinen StringBuilder-rakennustyökalu) {if (builder == null) {heitä uusi IllegalArgumentException ("Annettu StringBuilder oli tyhjä; ei-nolla-arvo on annettava."); } builder.append ("Kiitos StringBuilderin toimittamisesta."); } / ** * Liitä ennalta määritelty merkkijono annettuun StringBuilderiin. * * @param builder StringBuilder, johon on liitetty teksti; pitäisi olla * nolla. * / private void appendPredefinedTextToProvidedBuilderNoCheckForNull (viimeinen StringBuilder-rakennustyökalu) {builder.append ("Kiitos StringBuilderin toimittamisesta."); } / ** * Osoita parametrien nollan tarkistuksen vaikutus ennen kuin yrität käyttää * mahdollisesti syötettyjä parametreja, jotka ovat nollia. * / public void demonstrCheckingArgumentsForNull () {final String reasonStr = "anna null menetelmälle argumenttina."; logHeader ("DEMONSTRATING CHECKING METHOD PARAMETERS for NULL", System.out); kokeile {appendPredefinedTextToProvidedBuilderNoCheckForNull (null); } catch (NullPointerException nullPointer) {loki (causStr, nullPointer, System.out); } kokeile {appendPredefinedTextToProvidedBuilderCheckForNull (null); } catch (IllegalArgumentException laitonArgument) {loki (causStr, laitonArgument, System.out); }} 

Kun yllä oleva koodi on suoritettu, lähtö näkyy seuraavan kuvan mukaisesti.

VIRHE: NullPointerException ilmeni, kun yritettiin antaa argumentille nolla menetelmä -menetelmälle. java.lang.NullPointerException VIRHE: IllegalArgumentException havaittu yritettäessä antaa null-menetelmä argumentiksi. java.lang.IllegalArgumentException: Annettu StringBuilder oli tyhjä; ei-nolla-arvo on annettava. 

Molemmissa tapauksissa virheilmoitus kirjattiin. Tapaus, jossa nolla tarkistettiin, heitti kuitenkin mainostetun IllegalArgumentExceptionin, joka sisälsi lisätietoja asiasta siitä, milloin nolla havaittiin. Vaihtoehtoisesti tämä null-parametri olisi voitu käsitellä monin eri tavoin. Tapauksessa, jossa null-parametria ei käsitelty, ei ollut vaihtoehtoja sen käsittelemiseksi. Monet ihmiset haluavat heittää a NullPolinterException - kontekstitietojen kanssa, kun nolla havaitaan nimenomaisesti (katso kohta 60) Tehokas Java tai tuote nro 42 ensimmäisessä versiossa), mutta minulla on hieman etusija IllegalArgumentException kun se on nimenomaisesti menetelmän argumentti, joka on nolla, koska mielestäni aivan poikkeus lisää kontekstin yksityiskohtia ja "null" on helppo sisällyttää aiheeseen.

Menetelmä argumenttien nollatarkistustekniikka on oikeastaan ​​osa joukkoa yleisemmästä tekniikasta, jolla kaikki objektit tarkistetaan nollalle. Kuten edellä on hahmoteltu, julkisesti paljastettujen menetelmien argumentit ovat usein vähiten luotettavia sovelluksessa, joten niiden tarkistaminen voi olla tärkeämpää kuin keskimääräisen objektin nollan tarkistaminen.

Menetelmäparametrien nollan tarkistus on myös osa yleisempää käytäntöä menetelmän parametrien yleisen pätevyyden tarkistamiseksi, kuten on käsitelty Tehokas Java (Ensimmäisen painoksen kohta 23).

Harkitse alkukantoja esineiden sijaan

Mielestäni ei ole hyvä idea valita primitiivinen tietotyyppi (kuten int) vastaavan objektiviittaustyypin (kuten kokonaisluku) yli yksinkertaisesti a: n mahdollisuuden välttämiseksi NullPointerException, mutta ei voida kiistää, että yksi primitiivisten tyyppien eduista on se, etteivät ne johda NullPointerExceptions. Primitiivien pätevyys on kuitenkin tarkistettava (kuukausi ei voi olla negatiivinen kokonaisluku), joten hyöty voi olla pieni. Toisaalta primitiivejä ei voida käyttää Java-kokoelmissa, ja joskus halutaan kykyä asettaa arvo nollaksi.

Tärkeintä on olla hyvin varovainen primitiivien, viitetyyppien ja autoboksin yhdistelmässä. Sisään on varoitus Tehokas Java (Toinen painos, kohta # 49) koskien vaaroja, mukaan lukien heittäminen NullPointerException, joka liittyy primitiivisten ja vertailutyyppien huolimattomaan sekoittamiseen.

Harkitse tarkkaan ketjutettujen menetelmien puhelut

A NullPointerException voi olla erittäin helppo löytää, koska rivinumero ilmoittaa missä se tapahtui. Esimerkiksi pinon jäljitys saattaa näyttää seuraavalta:

java.lang.NullPointerException at dustin.examples.AvoidingNullPointerExamples.demonstrateNullPointerExceptionStackTrace (AvoidingNullPointerExamples.java:222) osoitteessa dustin.examples.AvoidingNullPointerExamx.x (esimerkkejä. 

Pinon jäljitys tekee selväksi, että NullPointerException heitettiin koodin seurauksena, joka suoritettiin rivillä 222 VältäNullPointerExamples.java. Jopa annetulla rivinumerolla voi silti olla vaikeaa kaventaa mitä null-objektia, jos on olemassa useita objekteja, joilla on menetelmiä tai kenttiä samalla rivillä.

Esimerkiksi lause, kuten someObject.getObjectA (). getObjectB (). getObjectC (). toString (); on neljä mahdollista puhelua, jotka ovat saattaneet heittää NullPointerException samalle koodiriville. Virheenkorjaimen käyttäminen voi auttaa tässä, mutta saattaa olla tilanteita, joissa on parempi hajottaa yllä oleva koodi yksinkertaisesti siten, että kukin puhelu suoritetaan erillisellä linjalla. Tämän avulla pinon jäljitykseen sisältyvä linjanumero osoittaa helposti, mikä tarkka puhelu oli ongelma. Lisäksi se helpottaa kunkin objektin nimenomaista tarkistamista tyhjäksi. Haittapuolena on kuitenkin, että koodin hajottaminen lisää koodiriviä (joillekin se on positiivista!) Eikä se välttämättä ole aina toivottavaa, varsinkin jos on varmaa, ettei mikään kyseisistä menetelmistä ole koskaan nolla.

Tee NullPointerExceptionsista informatiivisempia

Edellä mainitussa suosituksessa varoituksena oli harkita menetelmäpuhelun ketjuttamisen huolellista käyttöä ensisijaisesti siksi, että se teki linjanumeron pitoon NullPointerException vähemmän hyödyllinen kuin muuten voisi olla. Rivinumero näkyy kuitenkin pinon jäljityksessä vain, kun koodi käännettiin virheenkorjauslipun ollessa päällä. Jos se käännettiin ilman virheenkorjausta, pinon jäljitys näyttää seuraavalta:

java.lang.NullPointerException at dustin.examples.AvoidingNullPointerExamples.demonstrateNullPointerExceptionStackTrace (tuntematon lähde) osoitteessa dustin.examples.AvoidingNullPointerExamples.main (tuntematon lähde) 

Kuten yllä oleva tulos osoittaa, menetelmälle on olemassa nimi, mutta ei rivinumeroa NullPointerException. Tämän vuoksi on vaikeampaa tunnistaa välittömästi, mikä koodissa johti poikkeukseen. Yksi tapa puuttua tähän on antaa kontekstitietoja missä tahansa heitetyssä NullPointerException. Tämä ajatus osoitettiin aikaisemmin, kun a NullPointerException pyydettiin ja heitettiin uudelleen kontekstitietojen kanssa a IllegalArgumentException. Vaikka poikkeus yksinkertaisesti heitetään uudelleen toiseksi NullPointerException kontekstitietojen kanssa se on silti hyödyllinen. Kontekstitiedot auttavat koodin virheenkorjaajaa tunnistamaan ongelman todellisen syyn nopeammin.

Seuraava esimerkki osoittaa tämän periaatteen.

lopullinen kalenteri nullCalendar = null; kokeile {viimeinen päivämäärä päivämäärä = nullCalendar.getTime (); } catch (NullPointerException nullPointer) {log ("NullPointerException hyödyllisten tietojen kanssa", nullPointer, System.out); } kokeile {if (nullCalendar == null) {heitä uusi NullPointerException ("Päivämäärää ei voitu purkaa toimitetusta kalenterista"); } viimeinen päivämäärä päivämäärä = nullCalendar.getTime (); } catch (NullPointerException nullPointer) {log ("NullPointerException hyödyllisten tietojen kanssa", nullPointer, System.out); } 

Yllä olevan koodin suorittamisen tulos näyttää seuraavalta.

VIRHE: NullPointerException havaittu yritettäessä NullPointerException hyödyllisten tietojen kanssa java.lang.NullPointerException VIRHE: NullPointerException havaittu yritettäessä NullPointerException hyödyllisten tietojen kanssa java.lang.NullPointerException annettu: Kalenteria ei voitu purkaa päivämäärästä