Ohjelmointi

Varo yleisten poikkeusten vaaroja

Työskennellessäni äskettäisen projektin kanssa löysin koodinpätkän, joka suoritti resurssien puhdistamisen. Koska sillä oli monia erilaisia ​​puheluita, se saattoi aiheuttaa kuusi erilaista poikkeusta. Alkuperäinen ohjelmoija yritti yksinkertaistaa koodia (tai vain tallentaa kirjoituksen) ilmoitti, että menetelmä heittää Poikkeus pikemminkin kuin kuusi erilaista poikkeusta, jotka voitaisiin heittää. Tämä pakotti kutsukoodin käärimään kiinni yritetty / kiinni-lohkoon Poikkeus. Ohjelmoija päätti, että koska koodi oli tarkoitettu puhdistustarkoituksiin, vikatapaukset eivät olleet tärkeitä, joten lukituslohko pysyi tyhjänä järjestelmän sammuessa.

Nämä eivät tietenkään ole parhaita ohjelmointikäytäntöjä, mutta mikään ei näytä olevan kovin väärin ... paitsi pieni logiikkaongelma alkuperäisen koodin kolmannella rivillä:

Luettelo 1. Alkuperäinen puhdistuskoodi

private void cleanupConnections () heittää ExceptionOne, ExceptionTwo {for (int i = 0; i <yhteydet.pituus; i ++) {yhteys [i] .release (); // Heittää ExceptionOne, ExceptionTwo-yhteyden [i] = null; } yhteydet = nolla; } suojattu abstrakti void cleanupFiles () heittää ExceptionThree, ExceptionFour; suojattu abstrakti void removeListeners () heittää ExceptionFive, ExceptionSix; public void cleanupKaikki () heittää poikkeuksen {cleanupConnections (); cleanupFiles (); removeListeners (); } public void done () {kokeile {doStuff (); siivousKaikki (); doMoreStuff (); } saalis (poikkeus e) {}} 

Koodin toisessa osassa liitännät taulukkoa ei alusteta ennen kuin ensimmäinen yhteys on luotu. Mutta jos yhteyttä ei koskaan luoda, yhteysryhmä on tyhjä. Joten joissakin tapauksissa kutsu yhteydet [i]. vapauta () johtaa a NullPointerException. Tämä on suhteellisen helppo korjata ongelma. Lisää vain sekki yhteydet! = nolla.

Poikkeuksesta ei kuitenkaan koskaan ilmoiteta. Sen heittää cleanupConnections (), heitti taas siivousKaikki ()ja lopulta kiinni tehty(). tehty() menetelmä ei tee mitään lukuun ottamatta, se ei edes kirjaa sitä. Ja koska siivousKaikki () kutsutaan vain läpi tehty(), poikkeusta ei ole koskaan nähty. Joten koodi ei koskaan korjaudu.

Epäonnistumisskenaariossa cleanupFiles () ja removeListeners () menetelmiä ei koskaan kutsuta (joten niiden resursseja ei koskaan vapauteta), ja doMoreStuff () ei näin ollen koskaan kutsuttu lopulliseksi prosessoinniksi tehty() ei koskaan valmis. Asiaa pahentaa, tehty() ei soiteta, kun järjestelmä sammuu; sen sijaan sitä kutsutaan suorittamaan kaikki tapahtumat. Joten resursseja vuotaa jokaisessa tapahtumassa.

Tämä ongelma on selvästi merkittävä: virheistä ei ilmoiteta ja resursseja vuotaa. Mutta koodi itse näyttää melko viattomalta, ja koodin kirjoittamistavasta tämä ongelma osoittautuu vaikeaksi jäljittää. Muutama yksinkertainen ohjeisto soveltuu kuitenkin ongelman löytämiseen ja korjaamiseen:

  • Älä ohita poikkeuksia
  • Älä ota yleistä Poikkeuss
  • Älä heitä yleisiä Poikkeuss

Älä ohita poikkeuksia

Listing 1: n koodin ilmeisin ongelma on, että ohjelmassa oleva virhe jätetään kokonaan huomiotta. Odottamaton poikkeus (poikkeukset ovat luonteeltaan odottamattomia) heitetään, eikä koodi ole valmis käsittelemään tätä poikkeusta. Poikkeuksesta ei edes ilmoiteta, koska koodissa oletetaan, että odotetuilla poikkeuksilla ei ole seurauksia.

Useimmissa tapauksissa poikkeus tulisi ainakin kirjata. Useat lokipaketit (katso sivupalkin "Kirjauspoikkeukset") voivat kirjata järjestelmävirheet ja poikkeukset vaikuttamatta merkittävästi järjestelmän suorituskykyyn. Suurin osa kirjausjärjestelmistä sallii myös pinonjälkien tulostamisen, mikä antaa arvokasta tietoa siitä, missä ja miksi poikkeus tapahtui. Lopuksi, koska lokit kirjoitetaan tyypillisesti tiedostoihin, poikkeustietue voidaan tarkistaa ja analysoida. Katso sivupalkin luettelosta 11 esimerkki pinon jälkien kirjaamisesta.

Poikkeusten kirjaaminen ei ole kriittistä joissakin erityistilanteissa. Yksi näistä on resurssien puhdistaminen viimeisessä lausekkeessa.

Lopulta poikkeukset

Luettelossa 2 jotkut tiedot luetaan tiedostosta. Tiedoston on suljettava riippumatta siitä, lukeeko poikkeus tietoja, joten kiinni() menetelmä on kääritty viimeiseen lausekkeeseen. Mutta jos virhe sulkee tiedoston, siihen ei voida tehdä paljon:

Listaus 2

public void loadFile (String fileName) heittää IOException {InputStream in = null; kokeile {in = uusi FileInputStream (tiedostonimi); readSomeData (sisään); } lopuksi {if (in! = null) {kokeile {in.close (); } catch (IOException ioe) {// Ohitettu}}}} 

Ota huomioon, että loadFile () raportoi edelleen IOException kutsumenetelmään, jos todellinen tiedonlataus epäonnistuu I / O (tulo / lähtö) -ongelman vuoksi. Huomaa myös, että vaikka poikkeus kiinni() ohitetaan, koodi kertoo sen nimenomaisesti kommentissa, jotta se olisi selkeä kaikille koodin parissa työskenteleville. Voit soveltaa samaa menettelyä kaikkien I / O-virtojen puhdistamiseen, liittimien sulkemiseen ja JDBC-yhteyksiin jne.

Tärkeä asia poikkeusten ohittamisessa on varmistaa, että vain yksi menetelmä kääritään ohitettavaan try / catch-lohkoon (joten muita menetelmiä kutsutaan edelleen sulkulohkoon) ja että tietty poikkeus jää kiinni. Tämä erityinen olosuhde eroaa selvästi yleisen saamisesta Poikkeus. Kaikissa muissa tapauksissa poikkeuksen tulee olla (ainakin) kirjattuna, mieluiten pinojäljellä.

Älä löydä yleisiä poikkeuksia

Usein monimutkaisissa ohjelmistoissa tietty koodilohko suorittaa menetelmiä, jotka aiheuttavat erilaisia ​​poikkeuksia. Luokan dynaaminen lataaminen ja objektin ilmentäminen voi aiheuttaa useita erilaisia ​​poikkeuksia, mukaan lukien ClassNotFoundException, Välitön poikkeus, IllegalAccessExceptionja ClassCastException.

Sen sijaan, että lisätään neljä erilaista salauslohkoa kokeilulohkoon, varattu ohjelmoija voi yksinkertaisesti kääriä metodikutsut try / catch-lohkoon, joka tarttuu yleisiin Poikkeuss (katso alla oleva luettelo 3). Vaikka tämä tuntuu vaarattomalta, seurauksena voi olla joitain tahattomia sivuvaikutuksia. Esimerkiksi jos luokan nimi() on nolla, Class.forName () heittää a NullPointerException, joka jää kiinni menetelmästä.

Siinä tapauksessa saalislohko saa kiinni poikkeuksista, joita se ei ole koskaan aikonut saada kiinni, koska a NullPointerException on ryhmän alaluokka Ajonaikainen poikkeus, joka puolestaan ​​on alaluokka Poikkeus. Joten yleinen saalis (poikkeus e) sieppaa kaikki alaluokat Ajonaikainen poikkeusmukaan lukien NullPointerException, IndexOutOfBoundsExceptionja ArrayStoreException. Tyypillisesti ohjelmoija ei aio saada kiinni näistä poikkeuksista.

Luettelossa 3 null className johtaa a NullPointerException, joka osoittaa kutsumenetelmälle, että luokan nimi on virheellinen:

Listaus 3

public SomeInterface buildInstance (String className) {SomeInterface impl = null; kokeile {Class clazz = Class.forName (luokanNimi); impl = (SomeInterface) clazz.newInstance (); } catch (Poikkeus e) {log.error ("Virhe luokan luomisessa:" + luokanNimi); } paluu impl; } 

Toinen seuraus yleisestä saalislausekkeesta on, että hakkuita on rajoitettu, koska saada kiinni ei tiedä, että tietty poikkeus on kiinni. Jotkut ohjelmoijat, kun kohtaavat tämän ongelman, turvautuvat lisäämällä valinnan nähdäksesi poikkeustyypin (katso luettelo 4), mikä on ristiriidassa saalislohkojen käyttötarkoituksen kanssa:

Listaus 4

catch (Exception e) {if (ClassNotFoundException e e) {log.error ("Virheellinen luokan nimi:" + luokanNimi + "," + e.toString ()); } else {log.error ("Luokkaa ei voi luoda:" + luokanNimi + "," + e.toString ()); }} 

Listaus 5 tarjoaa täydellisen esimerkin tiettyjen poikkeusten saamisesta, joista ohjelmoija saattaa olla kiinnostunut esiintymä operaattoria ei vaadita, koska erityiset poikkeukset ovat kiinni. Jokainen tarkastetuista poikkeuksista (ClassNotFoundException, Välitön poikkeus, IllegalAccessException) on kiinni ja käsitelty. Erityistapaus, joka tuottaisi a ClassCastException (luokka latautuu kunnolla, mutta ei toteuta SomeInterface käyttöliittymä) tarkistetaan myös tarkistamalla kyseinen poikkeus:

Listaus 5

public SomeInterface buildInstance (String className) {SomeInterface impl = null; kokeile {Class clazz = Class.forName (luokanNimi); impl = (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.error ("Virheellinen luokan nimi:" + luokanNimi + "," + e.toString ()); } catch (InstantiationException e) {log.error ("Luokkaa ei voi luoda:" + luokanNimi + "," + e.toString ()); } catch (IllegalAccessException e) {log.error ("Ei voida luoda luokkaa:" + luokanNimi + "," + e.toString ()); } catch (ClassCastException e) {log.error ("Virheellinen luokan tyyppi," + className + "ei toteuta" + SomeInterface.class.getName ()); } paluu impl; } 

Joissakin tapauksissa on suositeltavaa uudistaa tunnettu poikkeus (tai ehkä luoda uusi poikkeus) kuin yrittää käsitellä sitä menetelmässä. Tämä antaa soittomenetelmälle mahdollisuuden käsitellä virhetilaa asettamalla poikkeuksen tunnetulle kontekstille.

Alla oleva luettelo 6 tarjoaa vaihtoehtoisen version buildInterface () menetelmä, joka heittää a ClassNotFoundException jos luokan lataamisen ja instantioinnin aikana ilmenee ongelma. Tässä esimerkissä soittomenetelmä taataan vastaanottavan joko oikein instansoitu objekti tai poikkeus. Siksi kutsumenetelmän ei tarvitse tarkistaa, onko palautettu objekti tyhjä.

Huomaa, että tämä esimerkki käyttää Java 1.4 -menetelmää uuden poikkeuksen ympärille luodun uuden poikkeuksen säilyttämiseksi alkuperäisen pinon jäljitystiedot. Muuten pinon jäljitys osoittaisi menetelmän buildInstance () menetelmänä, josta poikkeus on peräisin, heittämän taustalla olevan poikkeuksen sijasta newInstance ():

Listaus 6

public SomeInterface buildInstance (String luokan nimi) heittää ClassNotFoundException {kokeile {Class clazz = Class.forName (className); return (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.error ("Virheellinen luokan nimi:" + luokanNimi + "," + e.toString ()); heittää e; } catch (InstantiationException e) {heitä uusi ClassNotFoundException ("Ei voida luoda luokkaa:" + luokan nimi, e); } catch (IllegalAccessException e) {heitä uusi ClassNotFoundException ("Luokkaa ei voi luoda:" + luokan nimi, e); } catch (ClassCastException e) {heitä uusi ClassNotFoundException (className + "ei toteuta" + SomeInterface.class.getName (), e); }} 

Joissakin tapauksissa koodi voi pystyä palautumaan tietyistä virhetilanteista. Näissä tapauksissa tiettyjen poikkeusten saaminen on tärkeää, jotta koodi voi selvittää, onko ehto palautettavissa. Katso tämä mielessä luettelon 6 luokan esimerkki.

Luettelossa 7 koodi palauttaa oletusobjektin virheelliselle luokan nimi, mutta heittää poikkeuksen laittomista toiminnoista, kuten virheellisestä näyttelijästä tai tietoturvaloukkauksesta.

merkintä:Laiton luokkaPoikkeus on verkkotunnuksen poikkeusluokka, joka mainitaan tässä esittelytarkoituksiin.

Listaus 7

public SomeInterface buildInstance (String className) heittää IllegalClassException {SomeInterface impl = null; kokeile {Class clazz = Class.forName (luokanNimi); return (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.warn ("Virheellinen luokan nimi:" + luokanNimi + ", käyttäen oletusarvoa"); } catch (InstantiationException e) {log.warn ("Virheellinen luokan nimi:" + luokanNimi + ", käyttäen oletusarvoa"); } catch (IllegalAccessException e) {heitä uusi IllegalClassException ("Ei voida luoda luokkaa:" + luokan nimi, e); } catch (ClassCastException e) {heitä uusi IllegalClassException (className + "ei toteuta" + SomeInterface.class.getName (), e); } if (impl == null) {impl = uusi DefaultImplemantation (); } return impl; } 

Milloin yleisiä poikkeuksia olisi pyydettävä

Tietyt tapaukset oikeuttavat, kun se on kätevää ja vaadittavaa yleisten saaliiden saamiseksi Poikkeuss. Nämä tapaukset ovat hyvin spesifisiä, mutta tärkeitä suurille, vikasietoisille järjestelmille. Luettelossa 8 pyynnöt luetaan pyyntöjen jonosta ja käsitellään järjestyksessä. Mutta jos pyynnön käsittelyn aikana tapahtuu poikkeuksia (joko a BadRequestException tai minkä tahansa alaluokka Ajonaikainen poikkeusmukaan lukien NullPointerException), niin poikkeus jää kiinni ulkopuolella käsittely silmukan aikana. Joten mikä tahansa virhe aiheuttaa käsittelysilmukan pysähtymisen ja kaikki jäljellä olevat pyynnöt ei käsiteltävä. Tämä on huono tapa käsitellä virhettä pyynnön käsittelyn aikana:

Listaus 8

public void processAllRequests () {Pyydä req = null; kokeile {while (true) {req = getNextRequest (); if (req! = null) {processRequest (req); // heittää BadRequestException} else {// Pyyntöjono on tyhjä, täytyy tehdä tauko; }}} catch (BadRequestException e) {log.error ("Virheellinen pyyntö:" + req, e); }} 
$config[zx-auto] not found$config[zx-overlay] not found