Java tukee tarkistettuja poikkeuksia. Jotkut rakastavat tätä kiistanalaista ominaisuutta ja toiset vihaavat siihen pisteeseen asti, jossa useimmat ohjelmointikielet välttävät tarkistettuja poikkeuksia ja tukevat vain heidän tarkistamattomia kollegoitaan.
Tässä viestissä tarkastelen kiistoja tarkastettujen poikkeusten ympärillä. Esittelen ensin poikkeuskäsitteen ja kuvaan lyhyesti Java-kielen tuen poikkeuksille auttaakseni aloittelijoita ymmärtämään paremmin kiistaa.
Mitä ovat poikkeukset?
Ihanteellisessa maailmassa tietokoneohjelmilla ei koskaan olisi mitään ongelmia: tiedostoja olisi olemassa, kun niiden oletetaan olevan olemassa, verkkoyhteydet eivät koskaan sulkeudu odottamatta, menetelmää ei koskaan yritetä kutsua nollaviitteen, kokonaislukujako - nolla yritystä ei tapahtuisi, ja niin edelleen. Maailmamme ei kuitenkaan ole läheskään ihanteellinen; nämä ja muut poikkeuksia ihanteelliseen ohjelman suoritukseen ovat yleisiä.
Varhaiset yritykset tunnistaa poikkeukset sisälsivät erityisarvojen palauttamisen, jotka osoittavat epäonnistumisen. Esimerkiksi C-kielen fopen ()
toiminto palaa TYHJÄ
kun se ei voi avata tiedostoa. Myös PHP: t mysql_query ()
toiminto palaa VÄÄRÄ
kun tapahtuu SQL-virhe. Todellista vikakoodia on etsittävä muualta. Vaikka tämä "palaa erityisarvoon" -lähestymistapa on helppo toteuttaa, poikkeusten tunnistamisessa on kaksi ongelmaa:
- Erityisarvot eivät kuvaa poikkeusta. Mikä tekee
TYHJÄ
taiVÄÄRÄ
todella tarkoittaa? Kaikki riippuu toiminnon tekijästä, joka palauttaa erikoisarvon. Lisäksi, miten suhteutat erityisarvon ohjelman kontekstiin poikkeuksen sattuessa, jotta voit esittää mielekästä viestiä käyttäjälle? - Erityisen arvon ohittaminen on liian helppoa. Esimerkiksi,
int c; FILE * fp = fopen ("data.txt", "r"); c = fgetc (fp);
on ongelmallinen, koska tämä C-koodin fragmentti suoritetaanfgetc ()
lukea merkki tiedostosta, vaikkafopen ()
palaaTYHJÄ
. Tässä tapauksessa,fgetc ()
ei onnistu: meillä on vika, jota voi olla vaikea löytää.
Ensimmäinen ongelma ratkaistaan käyttämällä luokkia kuvaamaan poikkeuksia. Luokan nimi identifioi poikkeuksen tyypin ja sen kentät yhdistävät sopivan ohjelmakontekstin sen määrittämiseksi (menetelmän kutsujen kautta), mikä meni pieleen. Toinen ongelma ratkaistaan kääntäjällä pakottaen ohjelmoijan joko vastaamaan poikkeukseen suoraan tai osoittamaan, että poikkeus on käsiteltävä muualla.
Jotkut poikkeukset ovat hyvin vakavia. Esimerkiksi ohjelma saattaa yrittää jakaa muistia, kun vapaata muistia ei ole käytettävissä. Rajaton rekursio, joka tyhjentää pinon, on toinen esimerkki. Tällaiset poikkeukset tunnetaan nimellä virheitä.
Poikkeukset ja Java
Java käyttää luokkia kuvaamaan poikkeuksia ja virheitä. Nämä luokat on järjestetty hierarkiaan, joka juurtuu java.lang.heitettävä
luokassa. (Syy, miksi Heitettävä
valittiin nimeämään tämä erikoisluokka käy pian ilmi.) Suoraan sen alapuolella Heitettävä
ovat java.lang.Poikkeus
ja java.lang.Error
luokat, jotka kuvaavat vastaavasti poikkeuksia ja virheitä.
Esimerkiksi Java-kirjasto sisältää java.net.URISyntaxException
, joka ulottuu Poikkeus
ja osoittaa, että merkkijonoa ei voitu jäsentää yhtenäisen resurssitunnisteen viitteenä. Ota huomioon, että URISyntaxException
noudattaa nimeämiskäytäntöä, jossa poikkeusluokan nimi päättyy sanaan Poikkeus
. Samanlainen käytäntö koskee virheluokkien nimiä, kuten java.lang.OutOfMemoryError
.
Poikkeus
on alaluokassa java.lang.RuntimeException
, joka on yliluokka niistä poikkeuksista, jotka voidaan heittää Java Virtual Machine (JVM) -järjestelmän normaalin toiminnan aikana. Esimerkiksi, java.lang.ArithmeticException
kuvaa aritmeettisia epäonnistumisia, kuten yrityksiä jakaa kokonaisluvut kokonaisluvulla 0. java.lang.NullPointerException
kuvaa yrityksiä käyttää objektin jäseniä nollaviitteen avulla.
Toinen tapa tarkastella Ajonaikainen poikkeus
Java 8 -kielimäärityksen osassa 11.1.1 todetaan: Ajonaikainen poikkeus
on yliluokka kaikista poikkeuksista, jotka voidaan heittää monista syistä ilmentymisen arvioinnin aikana, mutta joista palautuminen voi silti olla mahdollista.
Kun tapahtuu poikkeus tai virhe, sopivan objekti Poikkeus
tai Virhe
alaluokka luodaan ja välitetään JVM: lle. Esineen ohittaminen tunnetaan nimellä heittää poikkeuksen. Java tarjoaa heittää
tätä tarkoitusta varten. Esimerkiksi, heittää uusi IOException ("ei voi lukea tiedostoa");
luo uuden java.io.IOException
määritettyyn tekstiin alustettu objekti. Tämä esine heitetään myöhemmin JVM: lle.
Java tarjoaa yrittää
lauseke koodin rajaamiseksi, josta voidaan poiketa. Tämä lause koostuu avainsanasta yrittää
jota seuraa aaltosulkeilla erotettu lohko. Seuraava koodinpätkä osoittaa yrittää
ja heittää
:
kokeile {method (); } // ... void method () {heitä uusi NullPointerException ("tekstiä"); }
Tässä koodinpätkässä suoritus suoritetaan yrittää
estä ja kutsuu menetelmä()
, joka heittää esimerkin NullPointerException
.
JVM vastaanottaa heitettävä ja etsii menetelmä-kutsupinosta a käsittelijä käsitellä poikkeusta. Poikkeukset, joita ei ole johdettu Ajonaikainen poikkeus
käsitellään usein; ajonaikaisia poikkeuksia ja virheitä käsitellään harvoin.
Miksi virheitä käsitellään harvoin
Virheitä käsitellään harvoin, koska Java-ohjelma ei voi usein tehdä mitään virheen palautumiseksi. Esimerkiksi kun vapaa muisti on käytetty loppuun, ohjelma ei voi kohdistaa lisämuistia. Kuitenkin, jos allokointivirhe johtuu vapauttavan muistin pitämisestä kiinni, käskijä voi yrittää vapauttaa muistia JVM: n avulla. Vaikka käsittelijä saattaa näyttää olevan hyödyllinen tässä virhetilanteessa, yritys ei ehkä onnistu.
Käsittelijää kuvaa a saada kiinni
lohko, joka seuraa yrittää
lohko. saada kiinni
lohko tarjoaa otsikon, joka listaa ne poikkeustyypit, joita se on valmis käsittelemään. Jos heitettävän tyyppi sisältyy luetteloon, heitettävyys siirretään saada kiinni
estä, jonka koodi suoritetaan. Koodi reagoi vian syyn tavalla, joka saa ohjelman jatkumaan tai mahdollisesti lopettamaan:
kokeile {method (); } catch (NullPointerException npe) {System.out.println ("yritys käyttää objektin jäsentä tyhjän viitteen kautta"); } // ... void method () {heitä uusi NullPointerException ("tekstiä"); }
Tähän koodiosaan olen liittänyt a saada kiinni
estää yrittää
lohko. Kun NullPointerException
esine heitetään pois menetelmä()
, JVM etsii ja välittää suorituksen saada kiinni
lohko, joka lähettää viestin.
Lopuksi estää
A yrittää
lohko tai sen lopullinen saada kiinni
lohkoa voi seurata a lopulta
lohko, jota käytetään puhdistustehtävien suorittamiseen, kuten hankittujen resurssien vapauttamiseen. Minulla ei ole mitään muuta sanottavaa lopulta
koska sillä ei ole merkitystä keskustelussa.
Poikkeukset kuvannut Poikkeus
ja sen alaluokat paitsi Ajonaikainen poikkeus
ja sen alaluokat tunnetaan nimellä tarkistetut poikkeukset. Jokaiselle heittää
käsky, kääntäjä tutkii poikkeusobjektin tyypin. Jos tyyppi ilmoittaa tarkistetuksi, kääntäjä tarkistaa lähdekoodin varmistaakseen, että poikkeus käsitellään menetelmässä, johon se heitetään, tai ilmoitetaan käsiteltävän edelleen ylöspäin menetelmä-kutsupinossa. Kaikki muut poikkeukset tunnetaan nimellä tarkistamattomat poikkeukset.
Java antaa sinun ilmoittaa, että tarkistettu poikkeus käsitellään edelleen metod-call-pinossa ylöspäin liittämällä a heittää
lauseke (avainsana heittää
jota seuraa pilkulla erotettu luettelo tarkistetuista poikkeusluokkien nimistä) metodin otsikkoon:
kokeile {method (); } catch (IOException ioe) {System.out.println ("I / O-virhe"); } // ... void method () heittää IOException {heittää uuden IOException ("tekstiä"); }
Koska IOException
on tarkistettu poikkeustyyppi, tämän poikkeuksen heitetyt esiintymät on käsiteltävä menetelmässä, johon ne heitetään tai jotka on julistettava käsiteltäviksi edelleen ylöspäin menetelmä-kutsupinossa liittämällä heittää
jokaisen vaikutuksen kohteena olevan menetelmän otsikkoon. Tässä tapauksessa a heittää IOExceptionin
lauseke on liitetty menetelmä()
otsikko. Heitetty IOException
objekti välitetään JVM: lle, joka etsii ja siirtää suorituksen saada kiinni
käsittelijä.
Tarkistettujen poikkeusten puolesta ja vastaan
Tarkastetut poikkeukset ovat osoittautuneet erittäin kiistanalaisiksi. Ovatko ne hyviä kieliominaisuuksia vai ovatko he huonoja? Tässä osassa esitän tapaukset tarkastettujen poikkeusten puolesta ja vastaan.
Tarkistetut poikkeukset ovat hyviä
James Gosling loi Java-kielen. Hän sisällytti tarkistetut poikkeukset kannustamaan vankempien ohjelmistojen luomista. Vuonna 2003 käydyssä keskustelussa Bill Vennersin kanssa Gosling huomautti, kuinka helppoa on tuottaa vikakoodi C-kielellä jättämällä huomiotta C-tiedostopohjaisten toimintojen palauttamat erityisarvot. Esimerkiksi ohjelma yrittää lukea tiedostosta, jota ei avattu lukemista varten.
Palautusarvojen tarkistamisen vakavuus
Palautusarvojen tarkistamatta jättäminen ei ehkä näytä olevan iso juttu, mutta tällä huolimattomuudella voi olla elämän tai kuoleman seurauksia. Ajattele esimerkiksi sellaisia bugisia ohjelmistoja, jotka ohjaavat ohjusohjausjärjestelmiä ja kuljettajattomia autoja.
Gosling huomautti myös, että korkeakoulujen ohjelmointikursseilla ei keskustella riittävästi virheiden käsittelystä (vaikka se on saattanut muuttua vuodesta 2003). Kun käydät yliopiston läpi ja teet tehtäviä, he vain pyytävät koodaamaan yhden todellisen polun [toteutus, jossa epäonnistuminen ei ole näkökohta]. En todellakaan ole koskaan kokenut yliopistokurssia, jossa virheiden käsittelystä olisi lainkaan keskusteltu. Tulet yliopistosta ja ainoa juttu, jonka kanssa olet joutunut käsittelemään, on yksi todellinen polku.
Keskittyminen vain yhteen todelliseen polkuun, laiskuuteen tai muuhun tekijään on johtanut siihen, että kirjoitetaan paljon vikakoodia. Tarkistetut poikkeukset vaativat ohjelmoijan harkitsemaan lähdekoodin suunnittelua ja toivottavasti saavuttamaan vankemman ohjelmiston.
Tarkistetut poikkeukset ovat huonoja
Monet ohjelmoijat vihaavat tarkistettuja poikkeuksia, koska heidän on pakko käsitellä API: ita, jotka käyttävät niitä liikaa tai määrittelevät tarkistetut poikkeukset tarkistamattomien poikkeusten sijasta sopimuksissaan. Esimerkiksi menetelmä, joka asettaa anturin arvon, välitetään virheellinen numero ja heittää valitun poikkeuksen valitsemattoman esiintymän sijaan java.lang.IllegalArgumentException
luokassa.
Tässä on muutama muu syy, miksi valitut poikkeukset eivät pidä; Olen poiminut heidät Slashdotin haastatteluista: Kysy James Goslingilta Java- ja Ocean Exploring Robots -keskustelusta:
- Tarkistettuja poikkeuksia on helppo sivuuttaa antamalla ne uudelleen
Ajonaikainen poikkeus
tapauksia, niin mitä järkeä heillä on? Olen menettänyt lukumäärän siitä, kuinka monta kertaa olen kirjoittanut tämän koodilohkon:kokeile {// tehdä tavaraa} kiinni (ärsyttävätarkistettuException e) {heittää uusi RuntimeException (e); }
99% ajasta en voi tehdä mitään asialle. Lopuksi lohkot tekevät tarvittavan puhdistuksen (tai ainakin heidän pitäisi).
- Tarkistetut poikkeukset voidaan jättää huomiotta nielemällä ne, joten mitä järkeä niillä on? Olen myös menettänyt laskemiseni siitä, kuinka monta kertaa olen nähnyt tämän:
kokeile {// tehdä tavaraa} kiinni (AnnoyingCheckedException e) {// tee mitään}
Miksi? Koska jonkun oli käsiteltävä sitä ja hän oli laiska. Oliko se väärin? Varma. Tapahtuuko se? Ehdottomasti. Entä jos tämä olisi tarkistamaton poikkeus sen sijaan? Sovellus olisi juuri kuollut (mikä on parempi kuin poikkeuksen nieleminen).
- Tarkistetut poikkeukset johtavat moniin
heittää
lauseke-ilmoitukset. Tarkastettujen poikkeusten ongelma on, että ne kannustavat ihmisiä nielemään tärkeitä yksityiskohtia (nimittäin poikkeusluokan). Jos päätät olla nielemättä tätä yksityiskohtaa, sinun on jatkettava lisäämistäheittää
ilmoitukset koko sovelluksessasi. Tämä tarkoittaa 1) että uusi poikkeustyyppi vaikuttaa moniin funktion allekirjoituksiin, ja 2) voit jättää huomiotta tietyn poikkeuksen, jonka todella haluat saada (sanoa, että avaat toissijaisen tiedoston toiminnolle, joka kirjoittaa tietoja Toissijainen tiedosto on valinnainen, joten voit jättää huomiotta sen virheet, mutta koska allekirjoitus heittääIOException
, on helppo sivuuttaa tämä). - Tarkistetut poikkeukset eivät todellakaan ole poikkeuksia. Tarkastetuista poikkeuksista on se, että ne eivät ole oikeastaan poikkeuksia käsitteen tavanomaisen käsityksen mukaan. Sen sijaan ne ovat vaihtoehtoisia API-palautusarvoja.
Poikkeusten koko ajatus on, että virhe, joka heitetään jonnekin puheluketjussa, voi kuplia ylöspäin ja käsitellä koodilla jonnekin ylöspäin ilman, että väliintulevan koodin täytyy huolehtia siitä. Tarkastetut poikkeukset puolestaan edellyttävät, että heittimen ja sieppaajan välinen koodi on jokaisella tasolla ilmoittamaan tietävänsä kaikista poikkeusten muodoista, jotka voivat tapahtua niiden läpi. Tämä eroaa käytännössä vähän siitä, jos tarkistetut poikkeukset olivat yksinkertaisesti erityisiä palautusarvoja, jotka soittajan oli tarkistettava.
Lisäksi olen havainnut argumentin siitä, että sovellusten on käsiteltävä suuri määrä tarkistettuja poikkeuksia, jotka syntyvät useista kirjastoista, joihin he pääsevät. Tämä ongelma voidaan kuitenkin ratkaista älykkäästi suunnitellulla julkisivulla, joka hyödyntää Javan ketjutettuja poikkeuksia ja uudelleenkäyttöä, jotta voidaan vähentää huomattavasti käsiteltävien poikkeusten määrää säilyttäen alkuperäinen heitetty poikkeus.
Johtopäätös
Ovatko valitut poikkeukset hyviä vai ovatko ne huonoja? Toisin sanoen, pitäisikö ohjelmoijien pakottaa käsittelemään tarkistettuja poikkeuksia tai annettava heille mahdollisuus sivuuttaa ne? Pidän ajatuksesta ottaa käyttöön vankempia ohjelmistoja. Uskon kuitenkin myös, että Javan poikkeusten käsittelymekanismia on kehitettävä, jotta siitä tulisi ohjelmoijaystävällisempi. Tässä on pari tapaa parantaa tätä mekanismia: