Programavimas

Saugokitės bendrųjų išimčių pavojų

Dirbdamas prie naujausio projekto radau kodo dalį, kuri atliko išteklių valymą. Kadangi jis turėjo daug įvairių skambučių, tai galėjo sukelti šešias skirtingas išimtis. Originalus programuotojas, bandydamas supaprastinti kodą (arba tiesiog išsaugoti spausdinimą), pareiškė, kad metodas metamas Išimtis o ne šešias skirtingas išimtis, kurias būtų galima paminėti. Tai privertė skambinimo kodą suvynioti į užfiksuotą bandymo / gaudymo bloką Išimtis. Programuotojas nusprendė, kad kadangi kodas buvo skirtas valymo tikslams, gedimo atvejai nebuvo svarbūs, todėl sugavimo blokas liko tuščias, nes sistema vis tiek išsijungė.

Akivaizdu, kad tai nėra geriausia programavimo praktika, tačiau atrodo, kad niekas nėra labai blogai ... išskyrus nedidelę logikos problemą originalo kodo trečioje eilutėje:

Sąrašas 1. Originalus valymo kodas

private void cleanupConnections () meta ExceptionOne, ExceptionTwo {for (int i = 0; i <ryšiai.length; i ++) {connection [i] .release (); // meta „ExceptionOne“, „ExceptionTwo“ ryšį [i] = null; } jungtys = nulis; } apsaugotas abstraktus void cleanupFiles () meta ExceptionThree, ExceptionFour; apsaugotas abstraktus void removeListeners () meta ExceptionFive, ExceptionSix; public void cleanupViskas () išmeta išimtį {cleanupConnections (); valymasFiles (); pašalintiKlausytojai (); } public void done () {try {doStuff (); valymasViskas (); doMoreStuff (); } sugavimas (e išimtis) {}} 

Kitoje kodo dalyje jungtys masyvas nėra inicijuojamas, kol nėra sukurtas pirmasis ryšys. Bet jei ryšys niekada nesukuriamas, tada jungčių masyvas yra nulinis. Taigi kai kuriais atvejais skambutis jungtys [i] .release () rezultatas a „NullPointerException“. Tai palyginti lengva išspręsti problemą. Tiesiog pridėkite čekį jungtys! = niekinis.

Tačiau apie išimtį niekada nepranešama. Ją meta cleanupConnections (), vėl metamas valymasViskas ()ir pagaliau pagavo padaryta(). padaryta() metodas nieko nedaro, išskyrus išimtį, jis net neužsiregistruoja. Ir todėl valymasViskas () yra tik pašaukta padaryta(), išimties niekada nematyti. Taigi kodas niekada netaisomas.

Taigi pagal gedimo scenarijų cleanupFiles () ir pašalinti klausytojus () metodai niekada nevadinami (taigi jų ištekliai niekada neišleidžiami) ir „doMoreStuff“ () niekada nėra vadinamas galutiniu duomenų apdorojimu padaryta() niekada nebaigia. Dar blogiau, padaryta() nėra iškviečiamas, kai sistema išsijungia; vietoj to jis kviečiamas atlikti kiekvieną operaciją. Taigi ištekliai nuteka kiekvienoje operacijoje.

Ši problema akivaizdžiai yra pagrindinė: apie klaidas nepranešama, o ištekliai nuteka. Tačiau pats kodas atrodo gana nekaltas, ir iš to, kaip kodas buvo parašytas, šią problemą sunku atsekti. Tačiau taikant keletą paprastų gairių, problemą galima rasti ir išspręsti:

  • Neignoruokite išimčių
  • Nepagauti generinių Išimtiss
  • Nemeskite bendrinių Išimtiss

Neignoruokite išimčių

Akivaizdžiausia 1 sąrašo kodo problema yra ta, kad programos klaida yra visiškai ignoruojama. Numatoma netikėta išimtis (išimtys pagal savo pobūdį yra netikėtos) ir kodas nėra parengtas spręsti šią išimtį. Apie išimtį net nepranešama, nes kodas daro prielaidą, kad laukiamos išimtys neturės pasekmių.

Daugeliu atvejų išimtis turėtų būti užregistruota. Keli registravimo paketai (žr. Šoninę juostą „Prisijungimo išimtys“) gali registruoti sistemos klaidas ir išimtis, reikšmingai nepaveikdami sistemos našumo. Dauguma registravimo sistemų taip pat leidžia spausdinti rietuvių pėdsakus, taip suteikiant vertingos informacijos apie tai, kur ir kodėl atsirado išimtis. Galiausiai, kadangi žurnalai paprastai rašomi į bylas, galima peržiūrėti ir išanalizuoti išimčių įrašą. Žr. 11 sąrašo šoninėje juostoje pavyzdį, kaip kaupti kamino pėdsakus.

Išimčių registravimas nėra kritinis keliose konkrečiose situacijose. Vienas iš jų yra išteklių valymas pagaliau.

Galiausiai išimtys

2 sąraše kai kurie duomenys nuskaitomi iš failo. Failas turi būti uždarytas, neatsižvelgiant į tai, ar išimtis skaito duomenis, todėl Uždaryti() metodas yra įtrauktas į galutinę sąlygą. Bet jei klaida uždaro failą, nelabai ką galima padaryti:

2 sąrašas

public void loadFile (String fileName) išmeta IOException {InputStream in = null; pabandykite {in = new FileInputStream (fileName); readSomeData (in); } pagaliau {if (in! = null) {pabandykite {in.close (); } sugauti (IOException ioe) {// nepaisoma}}}} 

Prisimink tai loadFile () vis dar praneša apie IOException į iškvietimo metodą, jei faktinis duomenų įkėlimas nepavyksta dėl įvesties / išvesties (įvesties / išvesties) problemos. Taip pat atkreipkite dėmesį, kad nors ir išimtis Uždaryti() yra ignoruojamas, kode teigiama, kad komentare aiškiai nurodoma visiems, dirbantiems su kodu. Tą pačią procedūrą galite taikyti valydami visus įvesties / išvesties srautus, uždarydami lizdus ir JDBC jungtis ir pan.

Svarbus dalykas ignoruojant išimtis yra užtikrinti, kad ignoruojant bandymo / gaudymo bloką būtų įterptas tik vienas metodas (taigi vis dar vadinami kiti pridedamojo bloko metodai) ir kad būtų gauta konkreti išimtis. Ši ypatinga aplinkybė akivaizdžiai skiriasi nuo sugavimo generinių Išimtis. Visais kitais atvejais išimtis turėtų būti (bent jau) užregistruota, geriausia su kamino pėdsakais.

Negaukite bendrų išimčių

Dažnai sudėtingoje programinėje įrangoje tam tikras kodo blokas vykdo metodus, kurie sukelia daugybę išimčių. Dinamiškai pakraunant klasę ir pagreitinant objektą, gali būti kelios skirtingos išimtys, įskaitant „ClassNotFoundException“, InstantiationException, „IllegalAccessException“ir „ClassCastException“.

Užuot pridėjęs keturis skirtingus blokavimo blokus prie bandymo bloko, užimtas programuotojas gali paprasčiausiai suvynioti metodo iškvietimus į bandymo / gaudymo bloką, kuris sugauna bendrą Išimtiss (žr. 3 sąrašą žemiau). Nors tai atrodo nekenksminga, gali atsirasti tam tikrų nenumatytų šalutinių reiškinių. Pavyzdžiui, jei className () yra niekinis, „Class.forName“ () išmes a „NullPointerException“, kuris bus sugautas metodas.

Tokiu atveju sugavimo blokas gaudo išimtis, kurių niekada neketino sugauti, nes a „NullPointerException“ yra poklasis RuntimeException, kuris, savo ruožtu, yra poklasis Išimtis. Taigi bendrinis sugavimas (e išimtis) gaudo visus RuntimeException, įskaitant „NullPointerException“, IndexOutOfBoundsExceptionir „ArrayStoreException“. Paprastai programuotojas neketina gaudyti tų išimčių.

3 sąraše null className rezultatas a „NullPointerException“, kuris skambinimo metodui nurodo, kad klasės pavadinimas yra neteisingas:

3 sąrašas

public SomeInterface buildInstance (String className) {SomeInterface impl = null; pabandykite {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } catch (e išimtis) {log.error ("Klaida kuriant klasę:" + klasėsName); } grąžinimo impl; } 

Kita bendrosios sugavimo sąlygos pasekmė yra ta, kad kirtimai yra riboti, nes pagauti nežino, kokia konkreti išimtis buvo sugauta. Kai kurie programuotojai, susidūrę su šia problema, naudojasi pažymėdami, kad pamatytų išimties tipą (žr. 4 sąrašą), kuris prieštarauja sugavimo blokų naudojimo tikslui:

4 sąrašas

gaudyti (išimtis e) {if (e egzempliorius „ClassNotFoundException“) {log.error ("Neteisingas klasės pavadinimas:" + klasėsPavadinimas + "," + e.toString ()); } else {log.error ("Nepavyksta sukurti klasės:" + className + "," + e.toString ()); }} 

5 sąrašas pateikia išsamų pavyzdžių, kaip gaudyti konkrečias išimtis, kurios gali būti įdomios programuotojui egzempliorius operatorius nereikalingas, nes yra sugautos konkrečios išimtys. Kiekviena iš patikrintų išimčių („ClassNotFoundException“, InstantiationException, „IllegalAccessException“) yra sugautas ir su juo susidorota. Ypatingas atvejis, kuris sukeltų a „ClassCastException“ (klasė tinkamai įkeliama, bet neįdiegia „SomeInterface“ sąsaja) taip pat patikrinama tikrinant, ar nėra šios išimties:

5 sąrašas

public SomeInterface buildInstance (String className) {SomeInterface impl = null; pabandykite {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.error ("Neteisingas klasės pavadinimas:" + className + "," + e.toString ()); } catch (InstantiationException e) {log.error ("Nepavyksta sukurti klasės:" + className + "," + e.toString ()); } catch (IllegalAccessException e) {log.error ("Nepavyksta sukurti klasės:" + className + "," + e.toString ()); } catch (ClassCastException e) {log.error ("Neteisingas klasės tipas," + className + "neįgyvendina" + SomeInterface.class.getName ()); } grąžinimo impl; } 

Kai kuriais atvejais geriau pakartoti žinomą išimtį (arba galbūt sukurti naują išimtį), nei bandyti su ja elgtis. Tai leidžia skambinimo metodui tvarkyti klaidos sąlygą, išimtį įtraukiant į žinomą kontekstą.

Toliau pateiktame 6 sąraše pateikiama alternatyvi programos versija „buildInterface“ () metodas, kuris meta a „ClassNotFoundException“ jei kyla problema įkeliant ir greitinant klasę. Šiame pavyzdyje užtikrinama, kad skambinimo metodas gali gauti tinkamai išrinktą objektą arba išimtį. Taigi, iškvietimo metodo nereikia patikrinti, ar grąžinamas objektas yra nulinis.

Atminkite, kad šiame pavyzdyje naudojamas „Java 1.4“ metodas kuriant naują išimtį, apvyniotą kita išimtimi, siekiant išsaugoti pradinę kamino pėdsakų informaciją. Priešingu atveju, kamino pėdsakas parodytų metodą buildInstance () kaip metodas, iš kurio atsirado išimtis, o ne pagrindinė išimtis newInstance ():

6 sąrašas

public SomeInterface buildInstance (String className) meta ClassNotFoundException {pabandykite {Class clazz = Class.forName (className); return (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.error ("Neteisingas klasės pavadinimas:" + className + "," + e.toString ()); mesti e; } sugauti (InstantiationException e) {mesti naują ClassNotFoundException ("Nepavyksta sukurti klasės:" + klasėsVardas, e); } gaudyti (IllegalAccessException e) {mesti naują „ClassNotFoundException“ ("Nepavyksta sukurti klasės:" + klasėsVardas, e); } catch (ClassCastException e) {mest naują ClassNotFoundException (className + "neįgyvendina" + SomeInterface.class.getName (), e); }} 

Kai kuriais atvejais kodas gali atkurti po tam tikrų klaidų sąlygų. Tokiais atvejais svarbu gaudyti konkrečias išimtis, kad kodas galėtų išsiaiškinti, ar sąlyga yra atkuriama. Atsižvelgdami į tai, pažiūrėkite į 6 sąrašo klasės pavyzdžių pavyzdį.

7 sąraše kodas grąžina numatytąjį objektą kaip netinkamą className, bet išmeta išimtį neteisėtoms operacijoms, pvz., netinkamam dalyviui ar saugumo pažeidimui.

Pastaba:Neteisėta klasės išimtis yra čia paminėta domeno išimties klasė demonstravimo tikslais.

7 sąrašas

public SomeInterface buildInstance (String className) išmeta IllegalClassException {SomeInterface impl = null; pabandykite {Class clazz = Class.forName (className); return (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.warn ("Neteisingas klasės pavadinimas:" + className + ", naudojant numatytąjį"); } catch (InstantiationException e) {log.warn ("Netinkamas klasės pavadinimas:" + className + ", naudojant numatytąjį"); } pagauti (IllegalAccessException e) {mesti naują IllegalClassException ("Nepavyksta sukurti klasės:" + klasėsVardas, e); } sugauti (ClassCastException e) {mesti naują IllegalClassException (klasės pavadinimas + "neįgyvendina" + SomeInterface.class.getName (), e); } if (impl == null) {impl = new DefaultImplemantation (); } grąžinimo impl; } 

Kada turėtų būti sugautos bendrosios išimtys

Tam tikrais atvejais pateisinama, kada yra patogu ir reikalinga gaudyti generinius vaistus Išimtiss. Šie atvejai yra labai konkretūs, tačiau svarbūs didelėms, netoleruojančioms gedimų. 8 sąraše užklausos skaitomos iš užklausų eilės ir tvarkomos eilės tvarka. Bet jei apdorojant užklausą pasitaiko kokių nors išimčių (arba a „BadRequestException“ arba bet koks poklasis RuntimeException, įskaitant „NullPointerException“), tada ta išimtis bus sugauta lauke apdorojimas ciklo metu. Taigi dėl bet kokios klaidos apdorojimo ciklas sustoja ir visos likusios užklausos nedarys būti tvarkomi. Tai reiškia blogą klaidos apdorojimo būdą apdorojant užklausą:

8 sąrašas

public void processAllRequests () {Request req = null; pabandykite {while (true) {req = getNextRequest (); if (req! = null) {processRequest (req); // meta BadRequestException} else {// Užklausos eilė tuščia, turi būti padaryta pertrauka; }}} catch (BadRequestException e) {log.error ("Neteisinga užklausa:" + req, e); }}