Programmēšana

Uzmanieties no vispārējo izņēmumu bīstamības

Strādājot pie nesenā projekta, es atradu koda fragmentu, kas veica resursu tīrīšanu. Tā kā tam bija daudz dažādu zvanu, tas varētu radīt sešus dažādus izņēmumus. Sākotnējais programmētājs, mēģinot vienkāršot kodu (vai vienkārši saglabāt rakstīšanu), paziņoja, ka metode met Izņēmums nevis seši dažādi izņēmumi, kurus varētu izmest. Tas piespieda izsaukuma kodu ietīt mēģinājuma / ķeršanas blokā, kas noķēra Izņēmums. Programmētājs nolēma, ka, tā kā kods bija paredzēts tīrīšanas nolūkiem, atteices gadījumi nebija svarīgi, tāpēc uztveršanas bloks palika tukšs, jo sistēma tik un tā tika izslēgta.

Acīmredzot šī nav labākā programmēšanas prakse, taču šķiet, ka nekas nav ļoti nepareizi ... izņemot nelielu loģikas problēmu sākotnējā koda trešajā rindā:

Saraksts 1. Sākotnējais tīrīšanas kods

private void cleanupConnections () izmet ExceptionOne, ExceptionTwo {for (int i = 0; i <savienojumi.length; i ++) {savienojums [i] .release (); // izmet ExceptionOne, ExceptionTwo savienojums [i] = null; } savienojumi = null; } aizsargāts abstrakts void cleanupFiles () met ExceptionThree, ExceptionFour; aizsargāts abstrakts void removeListeners () met ExceptionFive, ExceptionSix; public void cleanupViss () met izņēmumu {cleanupConnections (); cleanupFiles (); noņemtKlausītāji (); } public void done () {mēģiniet {doStuff (); uzkopšanaViss (); doMoreStuff (); } nozveja (izņēmums e) {}} 

Citā koda daļā savienojumi masīvs netiek inicializēts, kamēr nav izveidots pirmais savienojums. Bet, ja savienojums nekad netiek izveidots, tad savienojumu masīvs nav derīgs. Tāpēc dažos gadījumos aicinājums uz savienojumi [i] .release () rezultātā a NullPointerException. Šī ir salīdzinoši viegli novēršama problēma. Vienkārši pievienojiet čeku savienojumi! = null.

Tomēr par izņēmumu nekad netiek ziņots. To izmet cleanupConnections (), atkal izmeta tīrīšanaViss (), un beidzot noķerti izdarīts (). The izdarīts () metode neko nedara, izņemot izņēmumu, tā pat netiek reģistrēta. Un tāpēc tīrīšanaViss () tiek izsaukts tikai cauri izdarīts (), izņēmums nekad nav redzams. Tātad kods nekad netiek fiksēts.

Tādējādi neveiksmes scenārijā cleanupFiles () un noņemtKlausītāji () metodes nekad netiek izsauktas (tāpēc viņu resursi nekad netiek atbrīvoti), un doMoreStuff () nekad netiek saukta par galīgo apstrādi izdarīts () nekad nepabeidz. Lai pasliktinātu situāciju, izdarīts () netiek izsaukts, kad sistēma izslēdzas; tā vietā tiek aicināts pabeigt katru darījumu. Tātad resursi noplūst katrā darījumā.

Šī problēma acīmredzami ir galvenā problēma: par kļūdām netiek ziņots, un resursi ir noplūduši. Bet pats kods šķiet diezgan nevainīgs, un, no tā, kā kods tika uzrakstīts, šī problēma izrādās grūti izsekojama. Tomēr, piemērojot dažas vienkāršas vadlīnijas, problēmu var atrast un novērst:

  • Neignorējiet izņēmumus
  • Neķeriet sugas Izņēmumss
  • Nemetiet vispārīgu Izņēmumss

Neignorējiet izņēmumus

Visredzamākā 1. saraksta koda problēma ir tā, ka tiek pilnībā ignorēta kļūda programmā. Tiek veikts negaidīts izņēmums (izņēmumi pēc būtības ir negaidīti), un kods nav gatavs rīkoties ar šo izņēmumu. Par izņēmumu pat netiek ziņots, jo kods pieņem, ka paredzamajiem izņēmumiem nebūs nekādu seku.

Vairumā gadījumu izņēmums ir jāreģistrē vismaz. Vairāki reģistrēšanas pakotnes (sk. Sānjoslu "Reģistrēšanas izņēmumi") var reģistrēt sistēmas kļūdas un izņēmumus, būtiski neietekmējot sistēmas veiktspēju. Lielākā daļa reģistrēšanas sistēmu ļauj drukāt arī kaudzes pēdas, tādējādi sniedzot vērtīgu informāciju par izņēmuma vietu un iemeslu. Visbeidzot, tā kā žurnāli parasti tiek ierakstīti failos, izņēmumu ierakstu var pārskatīt un analizēt. Skatiet sānjoslas 11. sarakstu, lai skatītu kaudzes pēdu reģistrēšanu.

Izņēmumu reģistrēšana nav kritiska dažās īpašās situācijās. Viens no tiem ir resursu tīrīšana galīgajā klauzulā.

Izņēmumi beidzot

2. sarakstā daži dati tiek nolasīti no faila. Fails ir jāaizver neatkarīgi no tā, vai izņēmums nolasa datus, tāpēc aizvērt () metode ir ietverta galīgajā klauzulā. Bet, ja kļūda aizver failu, neko daudz nevar darīt:

2. saraksts

public void loadFile (virknes faila nosaukums) izmet IOException {InputStream in = null; mēģiniet {in = new FileInputStream (fileName); readSomeData (in); } beidzot {if (in! = null) {mēģiniet {in.close (); } catch (IOException ioe) {// ignorēts}}}} 

Pieraksti to loadFile () joprojām ziņo par IOException uz izsaukšanas metodi, ja faktiskā datu ielāde neizdodas I / O (ievades / izvades) problēmas dēļ. Ņemiet vērā arī to, ka, kaut arī izņēmums no aizvērt () tiek ignorēts, kods komentārā skaidri norāda, lai tas būtu skaidrs ikvienam, kurš strādā pie koda. Jūs varat piemērot šo pašu procedūru visu I / O plūsmu tīrīšanai, kontaktligzdu un JDBC savienojumu aizvēršanai utt.

Izņēmumu ignorēšanā ir svarīgi nodrošināt, lai ignorēšanas mēģinājuma / uztveršanas blokā tiktu iesaiņota tikai viena metode (tātad joprojām tiek sauktas citas metodes pievienotajā blokā) un ka tiek pieķerts konkrēts izņēmums. Šis īpašais apstāklis ​​nepārprotami atšķiras no sugas ķeršanas Izņēmums. Visos citos gadījumos izņēmumam jābūt (vismaz) reģistrētam, vēlams ar kaudzes izsekošanu.

Nenonāciet vispārīgus izņēmumus

Bieži vien sarežģītā programmatūrā dotais koda bloks izpilda metodes, kas rada dažādus izņēmumus. Dinamiski ielādējot klasi un veicot objekta eksemplāru, var būt vairāki dažādi izņēmumi, tostarp ClassNotFoundException, InstantiationException, IllegalAccessException, un ClassCastException.

Tā vietā, lai mēģināšanas blokam pievienotu četrus dažādus uztveršanas blokus, aizņemts programmētājs var vienkārši ietīt metodes izsaukumus mēģinājuma / ķeršanas blokā, kas uztver vispārīgus Izņēmumss (skat. 3. sarakstu zemāk). Lai gan tas šķiet nekaitīgs, var rasties dažas neparedzētas blakusparādības. Piemēram, ja klases nosaukums () ir nulle, Class.forName () iemetīs a NullPointerException, kas tiks noķerts metodē.

Tādā gadījumā nozvejas blokā ir ietverti izņēmumi, kurus tā nekad nav plānojusi noķert, jo a NullPointerException ir apakšklase RuntimeException, kas, savukārt, ir Izņēmums. Tātad vispārīgais nozveja (izņēmums e) uztver visas APK apakšklases RuntimeException, ieskaitot NullPointerException, IndexOutOfBoundsException, un ArrayStoreException. Parasti programmētājs nedomā ķert šos izņēmumus.

3. Sarakstā null className rezultātā a NullPointerException, kas izsaukšanas metodei norāda, ka klases nosaukums nav derīgs:

3. saraksts

public SomeInterface buildInstance (String className) {SomeInterface impl = null; izmēģiniet {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } catch (izņēmums e) {log.error ("Kļūda, veidojot klasi:" + className); } atgriešanās impl; } 

Citas vispārējās nozvejas klauzulas sekas ir tādas, ka mežizstrāde ir ierobežota, jo noķert nezina, ka konkrētais izņēmums tiek noķerts. Daži programmētāji, saskaroties ar šo problēmu, pieliek pārbaudi, lai redzētu izņēmuma veidu (skat. 4. sarakstu), kas ir pretrunā ar nozvejas bloku izmantošanas mērķi:

4. saraksts

catch (izņēmums e) {if (eNosaukums ClassNotFoundException) {log.error ("Nederīgs klases nosaukums:" + className + "," + e.toString ()); } else {log.error ("Nevar izveidot klasi:" + className + "," + e.toString ()); }} 

5. Saraksts sniedz pilnīgu piemēru, kā noķert konkrētus izņēmumus, kas varētu interesēt programmētāju instanceof operators nav vajadzīgs, jo ir noķerti īpaši izņēmumi. Katrs no pārbaudītajiem izņēmumiem (ClassNotFoundException, InstantiationException, IllegalAccessException) tiek noķerts un ar to tiek galā. Īpašais gadījums, kas radītu a ClassCastException (klase tiek ielādēta pareizi, bet neievieš SomeInterface interfeiss) tiek pārbaudīts, pārbaudot arī šo izņēmumu:

5. saraksts

public SomeInterface buildInstance (String className) {SomeInterface impl = null; izmēģiniet {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.error ("Nederīgs klases nosaukums:" + className + "," + e.toString ()); } catch (InstantiationException e) {log.error ("Nevar izveidot klasi:" + className + "," + e.toString ()); } catch (IllegalAccessException e) {log.error ("Nevar izveidot klasi:" + className + "," + e.toString ()); } catch (ClassCastException e) {log.error ("Nederīgs klases tips," + className + "neievieš" + SomeInterface.class.getName ()); } atgriešanās impl; } 

Dažos gadījumos ir ieteicams pārzināt zināmu izņēmumu (vai varbūt izveidot jaunu izņēmumu), nevis mēģināt ar to rīkoties metodē. Tas ļauj izsaukšanas metodei apstrādāt kļūdas stāvokli, izņēmumu ievietojot zināmā kontekstā.

Zemāk esošajā 6 sarakstā ir pieejama alternatīva buildInterface () metode, kas met a ClassNotFoundException ja problēma rodas, ielādējot un atjaunojot klasi. Šajā piemērā izsaukšanas metode ir pārliecināta, ka tā saņem vai nu pareizi eksponētu objektu, vai izņēmumu. Tādējādi izsaukšanas metodei nav jāpārbauda, ​​vai atgrieztais objekts nav derīgs.

Ņemiet vērā, ka šajā piemērā tiek izmantota Java 1.4 metode, lai izveidotu jaunu izņēmumu, kas ietīts ap citu izņēmumu, lai saglabātu sākotnējo kaudzes izsekošanas informāciju. Pretējā gadījumā kaudzes izsekošana norādītu metodi buildInstance () kā metodi, no kuras radies izņēmums, nevis pamata izmestu newInstance ():

6. saraksts

public SomeInterface buildInstance (String klases nosaukums) izmet ClassNotFoundException {mēģiniet {Class clazz = Class.forName (className); return (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.error ("Nederīgs klases nosaukums:" + className + "," + e.toString ()); iemest e; } catch (InstantiationException e) {mest jaunu ClassNotFoundException ("Nevar izveidot klasi:" + klases nosaukums, e); } catch (IllegalAccessException e) {mest jaunu ClassNotFoundException ("Nevar izveidot klasi:" + klases nosaukums, e); } catch (ClassCastException e) {mest jaunu ClassNotFoundException (className + "neievieš" + SomeInterface.class.getName (), e); }} 

Dažos gadījumos kodu var atgūt pēc noteiktiem kļūdu apstākļiem. Šādos gadījumos ir svarīgi iegūt īpašus izņēmumus, lai kods varētu noskaidrot, vai nosacījums ir atkopjams. Paturot to prātā, skatiet klases ierakstu piemēru 6. sarakstā.

7. sarakstā kods atgriež noklusējuma objektu kā nederīgu className, bet izņēmums ir nelegālas darbības, piemēram, nederīgs dalībnieks vai drošības pārkāpums.

Piezīme:IllegalClassException ir domēna izņēmuma klase, kas šeit pieminēta demonstrācijas nolūkos.

7. saraksts

public SomeInterface buildInstance (String className) izmet IllegalClassException {SomeInterface impl = null; izmēģiniet {Class clazz = Class.forName (className); return (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.warn ("Nederīgs klases nosaukums:" + className + ", izmantojot noklusējumu"); } catch (InstantiationException e) {log.warn ("Nederīgs klases nosaukums:" + className + ", izmantojot noklusējumu"); } catch (IllegalAccessException e) {mest jaunu IllegalClassException ("Nevar izveidot klasi:" + klasesNOSAUKUMS, e); } catch (ClassCastException e) {mest jaunu IllegalClassException (className + "neievieš" + SomeInterface.class.getName (), e); } if (impl == null) {impl = new DefaultImplemantation (); } atgriešanās impl; } 

Kad jāķer vispārēji izņēmumi

Atsevišķi gadījumi attaisno, kad ir ērti un nepieciešams noķert sugas Izņēmumss. Šie gadījumi ir ļoti specifiski, bet svarīgi lielām sistēmām, kas izturīgas pret kļūmēm. 8. sarakstā pieprasījumi tiek nolasīti no pieprasījumu rindas un tiek apstrādāti secībā. Bet, ja pieprasījuma apstrādes laikā rodas kādi izņēmumi (vai nu a BadRequestException vai jebkurš apakšklase RuntimeException, ieskaitot NullPointerException), tad šis izņēmums tiks noķerts ārā apstrāde, kamēr cilpa. Tātad jebkura kļūda liek apstāties apstrādes ciklam un visiem atlikušajiem pieprasījumiem nebūs jāapstrādā. Tas ir slikts kļūdas apstrādes veids pieprasījuma apstrādes laikā:

8. saraksts

public void processAllRequests () {Pieprasīt req = null; mēģiniet {while (true) {req = getNextRequest (); if (req! = null) {processRequest (req); // izmet BadRequestException} else {// Pieprasījuma rinda ir tukša, jāizdara pauze; }}} catch (BadRequestException e) {log.error ("Nederīgs pieprasījums:" + req, e); }} 
$config[zx-auto] not found$config[zx-overlay] not found