Ohjelmointi

Ovatko valitut poikkeukset hyviä vai huonoja?

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Ä tai VÄÄ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 suoritetaan fgetc () lukea merkki tiedostosta, vaikka fopen () palaa TYHJÄ. 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: