Java podpira preverjene izjeme. Nekaterim je ta kontroverzna jezikovna značilnost všeč, drugi pa sovražijo do te mere, da se večina programskih jezikov izogiba preverjenim izjemam in podpira samo svoje nepreverjene kolege.
V tem prispevku preučujem polemiko glede preverjenih izjem. Najprej predstavim koncept izjem in na kratko opišem Javino jezikovno podporo izjemam, da bi začetnikom pomagal bolje razumeti spor.
Kaj so izjeme?
V idealnem svetu računalniški programi ne bi nikoli naleteli na kakršne koli težave: datoteke bi obstajale takrat, ko bi morale obstajati, omrežne povezave se ne bi nikoli nepričakovano zaprle, nikoli ne bi bilo poskusa priklicati metode prek ničelne reference, celoštevilčno deljenje-by - nič poskusov se ne bi zgodilo itd. Vendar naš svet še zdaleč ni idealen; te in druge izjeme do idealnega izvajanja programa so zelo razširjeni.
Zgodnji poskusi prepoznavanja izjem so vključevali vračanje posebnih vrednosti, ki kažejo na napako. Na primer jezik C fopen ()
funkcija vrne NIČ
ko ne more odpreti datoteke. Tudi PHP mysql_query ()
funkcija vrne NAPAKA
ko pride do napake SQL. Dejansko kodo napake morate poiskati drugje. Čeprav je enostaven za izvedbo, ima ta pristop k prepoznavanju izjem dve težavi:
- Posebne vrednosti ne opisujejo izjeme. Kaj je
NIČ
aliNAPAKA
res pomeni? Vse je odvisno od avtorja funkcije, ki vrne posebno vrednost. Poleg tega, kako povežete posebno vrednost s kontekstom programa, ko je prišlo do izjeme, tako da lahko uporabniku predstavite smiselno sporočilo? - Preveč enostavno je prezreti posebno vrednost. Na primer,
int c; FILE * fp = fopen ("data.txt", "r"); c = fgetc (fp);
je problematično, ker se ta fragment kode C izvedefgetc ()
za branje znaka iz datoteke, tudi kadarfopen ()
vrneNIČ
. V tem primeru,fgetc ()
ne bo uspelo: imamo napako, ki jo je težko najti.
Prvo težavo rešimo z uporabo razredov za opis izjem. Ime razreda opredeljuje vrsto izjeme in njegova polja združujejo ustrezen programski kontekst za ugotavljanje (s klici metode), kaj je šlo narobe. Druga težava se reši tako, da prevajalnik prisili programerja, da se bodisi neposredno odzove na izjemo bodisi navede, da bo izjemo treba obravnavati drugje.
Nekatere izjeme so zelo resne. Program lahko na primer poskuša dodeliti nekaj pomnilnika, kadar ni na voljo prostega pomnilnika. Drug primer je neomejena rekurzija, ki izčrpa kup. Takšne izjeme so znane kot napake.
Izjeme in Java
Java uporablja razrede za opis izjem in napak. Ti razredi so organizirani v hierarhijo, ki temelji na java.lang.Hitljivo
razred. (Razlog zakaj Mečljivo
je bil izbran za poimenovanje tega posebnega razreda, bo kmalu razvidno.) Neposredno spodaj Mečljivo
so java.lang.Exception
in java.lang.Error
razredi, ki opisujejo izjeme oziroma napake.
Na primer, knjižnica Java vključuje java.net.URISyntaxException
, ki se razteza Izjema
in označuje, da niza ni bilo mogoče razčleniti kot referenco enotnega identifikatorja vira. Upoštevajte to URISyntaxException
sledi dogovoru o poimenovanju, v katerem se ime razreda izjeme konča z besedo Izjema
. Podoben dogovor velja za imena razredov napak, kot je java.lang.OutOfMemoryError
.
Izjema
je podrazvrščeno z java.lang.RuntimeException
, ki je superrazred tistih izjem, ki jih lahko vržemo med običajnim delovanjem Java Virtual Machine (JVM). Na primer, java.lang.ArithmeticException
opisuje aritmetične napake, kot so poskusi delitve celih števil na celo število 0. java.lang.NullPointerException
opisuje poskuse dostopa do članov objekta prek ničelne reference.
Še en način gledanja RuntimeException
Oddelek 11.1.1 specifikacije jezika Java 8 določa: RuntimeException
je superrazred vseh izjem, ki jih lahko med vrednotenjem izraza vržemo iz številnih razlogov, vendar je izterjava morda še vedno mogoča.
Ko pride do izjeme ali napake, pride predmet iz ustreznega Izjema
ali Napaka
podrazred se ustvari in posreduje JVM. Dejanje podajanja predmeta je znano kot vrgel izjemo. Java ponuja vrgel
izjavo za ta namen. Na primer, vrzi nov IOException ("datoteke ni mogoče prebrati");
ustvari novo java.io.IOException
objekt, ki je inicializiran v določeno besedilo. Ta predmet se nato vrne v JVM.
Java ponuja poskusite
izjava za razmejitev kode, iz katere se lahko vrže izjema. Ta izjava je sestavljena iz ključne besede poskusite
čemur sledi blok, ločen z naramnicami. Naslednji fragment kode prikazuje poskusite
in vrgel
:
poskusite {method (); } // ... void method () {throw new NullPointerException ("nekaj besedila"); }
V tem fragmentu kode izvršitev vnese poskusite
blokira in prikliče metoda ()
, ki vrže primerek NullPointerException
.
JVM prejme mečljiv in išče sklad za klic metode za a vodnik za obravnavo izjeme. Izjeme, ki niso izpeljane iz RuntimeException
z njimi se pogosto ravna; Izjeme in napake med izvajanjem se redko obravnavajo.
Zakaj se napake redko obravnavajo
Napake se redko obravnavajo, ker program Java pogosto ne more storiti ničesar, da bi se obnovil po napaki. Na primer, ko prosti pomnilnik izčrpa, program ne more dodeliti dodatnega pomnilnika. Če pa je napaka pri dodeljevanju posledica zadrževanja veliko pomnilnika, ki ga je treba sprostiti, lahko hander poskuša osvoboditi pomnilnik s pomočjo JVM. Čeprav se v tem kontekstu napake zdi, da je uporabnik uporaben, poskus morda ne bo uspel.
Vodnika opisuje a ulov
blok, ki sledi poskusite
blok. The ulov
Blok vsebuje glavo, ki navaja vrste izjem, s katerimi je pripravljen. Če je vrsta metanja vključena na seznam, se metanje posreduje datoteki ulov
blok, katerega koda se izvede. Koda se odzove na vzrok okvare tako, da povzroči nadaljevanje ali morebitno zaustavitev programa:
poskusite {method (); } catch (NullPointerException npe) {System.out.println ("poskus dostopa do člana predmeta prek ničelne reference"); } // ... void method () {throw new NullPointerException ("nekaj besedila"); }
V ta fragment kode sem dodal ulov
blok do poskusite
blok. Ko NullPointerException
predmet je vržen iz metoda ()
, JVM najde in prenese izvedbo na ulov
blok, ki prikaže sporočilo.
Končno bloki
A poskusite
blok ali njegov končni ulov
bloku lahko sledi a končno
blok, ki se uporablja za izvajanje nalog čiščenja, na primer za sprostitev pridobljenih virov. Nimam več kaj povedati končno
ker to ni pomembno za razpravo.
Izjeme, ki jih je opisal Izjema
in njegovi podrazredi, razen RuntimeException
in njegovi podrazredi so znani kot preverjene izjeme. Za vsakogar vrgel
izjava, prevajalnik preuči vrsto predmeta izjeme. Če tip označi, da je preverjeno, prevajalnik preveri izvorno kodo, da zagotovi, da se izjema obravnava v metodi, kjer je vržena, ali je razglašena za nadaljnjo obdelavo klica metode. Vse druge izjeme so znane kot nepreverjene izjeme.
Java vam omogoča, da prijavite, da je preverjena izjema obdelana naprej v skladu s klicem metode tako, da dodate a meti
klavzulo (ključna beseda meti
čemur sledi seznam preverjenih imen razredov izjem z ločenimi vejicami) v glavo metode:
poskusite {method (); } catch (IOException ioe) {System.out.println ("V / I napaka"); } // ... void metoda () vrže IOException {vrže novo IOException ("nekaj besedila"); }
Ker IOException
je preverjena vrsta izjeme, vržene primere te izjeme je treba obdelati v metodi, kjer so vrženi, ali pa jih je treba razglasiti za nadaljnjo obdelavo klica metode, tako da dodate meti
v glavo vsake prizadete metode. V tem primeru a vrže IOException
je dodana klavzula metoda ()
je glava. Vrženi IOException
objekt se posreduje JVM, ki najde in prenese izvajanje na ulov
vodnik.
Trditev za in proti preverjenim izjemam
Izkazane izjeme so se izkazale za zelo kontroverzne. So dobra jezikovna značilnost ali so slabe? V tem poglavju predstavljam primere za in proti preverjenim izjemam.
Preverjene izjeme so dobre
James Gosling je ustvaril jezik Java. Vključil je preverjene izjeme, da bi spodbudil ustvarjanje močnejše programske opreme. Gosling je v pogovoru z Billom Vennersom leta 2003 poudaril, kako enostavno je ustvariti napačno kodo v jeziku C, tako da ignorira posebne vrednosti, ki jih vrnejo C-jeve datoteke usmerjene funkcije. Program na primer poskuša brati iz datoteke, ki ni bila uspešno odprta za branje.
Resnost nepreverjanja povratnih vrednosti
Nepreverjanje vrednosti vrnitve se morda ne zdi nič hudega, toda ta površnost ima lahko posledice za življenje ali smrt. Na primer, pomislite na takšno programsko opremo, ki nadzoruje sisteme za vodenje raket in avtomobile brez voznika.
Gosling je poudaril tudi, da tečaji univerzitetnega programiranja ne obravnavajo ustrezno obravnave napak (čeprav se je to morda spremenilo od leta 2003). Ko greš na fakulteto in opravljaš naloge, te samo prosijo, da kodiraš eno pravo pot [izvedbe, pri kateri neuspeh ni upoštevan]. Zagotovo nisem nikoli doživel univerzitetnega tečaja, kjer bi se sploh obravnavalo ravnanje z napakami. Prideš s fakultete in edina stvar, s katero si se moral spoprijeti, je resnična pot.
Če se osredotočimo samo na eno pravo pot, lenobo ali drug dejavnik, je bilo napisanih veliko napak. Označene izjeme zahtevajo, da programer upošteva zasnovo izvorne kode in upa, da bo dosegel bolj robustno programsko opremo.
Označene izjeme so slabe
Mnogi programerji sovražijo preverjene izjeme, ker so prisiljeni obravnavati API-je, ki jih prekomerno uporabljajo ali napačno določijo preverjene izjeme namesto nepreverjenih izjem kot del svojih pogodb. Na primer, metoda, ki nastavi vrednost senzorja, se prenese neveljavno število in vrže preverjeno izjemo namesto primerka nepreverjenega java.lang.IllegalArgumentException
razred.
Tu je še nekaj razlogov, zakaj ne marajo preverjenih izjem; Izvlekel sem jih iz intervjujev Slashdot: Vprašajte Jamesa Goslinga o razpravi o Java in Ocean Exploring Robots:
- Preverjene izjeme je enostavno prezreti, če jih znova vržemo kot
RuntimeException
primeri, torej kakšen smisel imajo ti? Izgubil sem število, kolikokrat sem napisal ta blok kode:poskusite {// narediti stvari} catch (NadleženException e) {metati nov RuntimeException (e); }
99% časa ne morem storiti ničesar glede tega. Končno bloki opravijo potrebno čiščenje (ali pa bi vsaj morali).
- Preverjene izjeme lahko prezrete tako, da jih pogoltnete, kaj je torej smisel, da jih imamo? Prav tako sem izgubil število, kolikokrat sem to videl:
poskusite {// do stuff} catch (AnnoyingCheckedException e) {// ne naredite nič}
Zakaj? Ker se je nekdo moral ukvarjati s tem in je bil len. Je bilo narobe? Seveda. Ali se to zgodi? Vsekakor. Kaj pa, če bi bila to nepreverjena izjema? Aplikacija bi pravkar umrla (kar je boljše kot pogoltniti izjemo).
- Izbrane izjeme povzročijo več
meti
izjave. Problem preverjenih izjem je, da spodbujajo ljudi, da pogoltnejo pomembne podrobnosti (in sicer razred izjem). Če se odločite, da teh podrobnosti ne boste pogoltnili, jih morate še naprej dodajatimeti
izjave v celotni aplikaciji. To pomeni 1) da bo nova vrsta izjeme vplivala na veliko podpisov funkcij in 2) lahko boste zamudili določen primerek izjeme, ki jo dejansko želite ujeti (recimo, da odprete sekundarno datoteko za funkcijo, ki zapisuje podatke v Sekundarna datoteka ni obvezna, zato lahko prezrete njene napake, ampak ker podpis vržeIOException
, tega je enostavno spregledati). - Označene izjeme v resnici niso izjeme. Stvar pri preverjenih izjemah je, da v resnici niso izjeme po običajnem razumevanju koncepta. Namesto tega gre za nadomestne povratne vrednosti API.
Celotna ideja izjem je, da se napaka, vržena nekje navzdol po klicni verigi, lahko sproži in jo koda obravnava nekje bolj navzgor, ne da bi jo bilo treba vmesni kodi skrbeti. Po drugi strani preverjene izjeme zahtevajo, da se na vsaki ravni kode med metalcem in lovilcem izjavi, da pozna vse oblike izjem, ki jih lahko preidejo. V praksi se to v resnici malo razlikuje od tega, če bi bile preverjene izjeme le posebne povratne vrednosti, ki jih je moral klicatelj preveriti.
Poleg tega sem naletel na argument, da morajo aplikacije obravnavati veliko število preverjenih izjem, ki so ustvarjene iz več knjižnic, do katerih dostopajo. Vendar je to težavo mogoče odpraviti s pametno zasnovano fasado, ki izkorišča Javino možnost priklenjenih izjem in ponovnega vračanja izjem, da se močno zmanjša število izjem, ki jih je treba obravnavati, hkrati pa ohraniti prvotno izjemo, ki je bila vržena.
Zaključek
Ali so preverjene izjeme dobre ali so slabe? Z drugimi besedami, ali bi morali biti programerji prisiljeni ravnati s preverjenimi izjemami ali jim dati možnost, da jih prezrejo? Všeč mi je ideja o uveljavljanju bolj robustne programske opreme. Vendar pa tudi mislim, da se mora Javin mehanizem za obravnavo izjem uporabljati tako, da postane bolj prijazen programerjem. Tu je nekaj načinov za izboljšanje tega mehanizma: