Изключително състояние е проблем, който не дава да продължи изпълнението на текущия метод или обхват. Важное да се различи изключителното състояние от обикновения проблем, в който има достатъчно информация в текущия контекст, даваща възможност нещо да се направи по въпроса. В изключителното състояние не може нищо да се направи поради липсата на информация в текущия контекст. Всичко което може да се направи е да се изскочи от текущия контекст и да се разгледа проблема в по-висок контекст. Това е, което се случва като се изхвърли изключение.
Прост пример е делението. Ако може да се опитате да делите на нула, може и да си струва да проверявате и да не продължите с делението. Но какво следва ако делителят е нула? Може да знаете, в контекста на конкретния метод, какво да се прави в такъв случай. Но може и да не знаете, ако такава стойност не се очаква и тогава трябва да изхвърлите изключение, а не да правите проверки.
Като изхвърлите изключение се случват няколко неща. Първо се създава обект на изключението по начин по който се създава всеки обект в Java: на хийпа, с new. После текущия път на изпълнение (този, който не може да продължи, запомнете) се спира и манипулатор към обекта на изключението се изтласква от текущия контекст. В този момент механизма за обслужване на изключенията влиза в действие и започва да търси подходящо място където програмата да продължи. Това подходящо място е exception handler-а, чиято задача е да се справи с проблема така, че програмата да може да вземе друг курс или просто да продължи.
Като прост пример на изхвърляне на изключение да вземем обектов манипулатор наречен t. Възможно е да се даде манипулатор, който не е бил инициализиран, така че може да пожелаете да правите проверка преди подаването на въпросния манипулатор като аргумент. Може да пратите информация за грешката в по-широкия контекст чрез създаване на обект представящ нужната информация и “изхвърлянето му” извън текущия контекст. Това се казва изхвърляне на изключение. Ето го как изглежда:
if(t == null)
throw new NullPointerException();
Това изхвърля изключение, като ви позволява – в текущия контекст – да абдикирате от отговорността да мислите повече за проблема. Той се оправя някакси магически някъде другаде. Скоро ще се покаже точно къде.
Аргументи на изключението
Вакто всеки обект в Java изключенията винаги се създават в хийпа с new и се вика конструктор. Има два конструктора за всички стандартни изключения; първият е по подразбиране, а вторият приема стринг за аргумент така че може да включите уместна информация в изключението:
if(t == null)
throw new NullPointerException("t = null");
Този стринг после може да бъде извлечен чрез различни методи както ще се покаже по-късно.
Ключовата дума throw причинява случването на относително магически неща. Първо се изпълнява new-израз за да се създаде обект, който не е там при нормалното изпълнение на програмата и, разбира се, конструктор се вика за обекта. После обекта, фактически, се “връща” от метода, макар и типът му да не е като този, закойто е проектиран метода. Опростен начин да се мисли за механизма на изключенията е да се смятат за друг начин на връщане от метод, макар и да има трудности ако се прокара тази аналогия твърде далеч. Може също да се излезе от най-вътрешния обхват чрез изхвърляне на изключение. Връща се стойност, а методът или обхватът завършват изпълнението си.
Всяка прилика с обикновеното завършване на метод свършва тук, понеже мястото където се връща изпълнението на програмата е напълно различнот от това при нормално завършване на метода. (Завършвате с подходящия обработчик на изключения който може да бъде на километри далеч – много нива позниско в стека на извикванията – от мястото където изкючението е изхвърлено.)
В добавка може да изхвърляте който тип от Throwable обект си искате. Типично ще изхвърляте различен тип изключение за различни типове грешки. Идеята е да се съхрани информация в обекта на изключението и в типа на избрания обект, така че някой в по-широкия контекст да може да разбере какво да прави с вашето изключение. (Често единствената информация е типа на обекта на изключението, а нищо значително не се съхранява в обекта.)
Хващане на изключение
Ако метод изхвъърли изключение той трябва да предполага, че то ще бъде хванато и обработено. Едно от предимствата на обработката на изключения в Java е, че позволява да се съсредоточите върху решаването на даден проблем на едно място, а после да се оправяте с грешките от този код на друго място.
За да видим как се хваща изключениетрябва първо да усвоим концепцията за guarded region, което е секция от код, която може да изхвърля изключения и е следвана от код, който обработва тези изключения.
Блокът try
Ако сте вътре в метод и изхвърлите изключение (или друг метод извикан извътре на този изхвърли изключение), методъът ще завърши като част от процеса на изхвърлянето. Ако не искате throw да напусне метод, може да напишете специален блок в метода който да хване изключението. Това го казват try block понеже “опитвате” различните извиквания на методи вътре. Трай блокът е обикновен обхват предшестван от ключовата дума try:
try {
// Code that might generate exceptions
}
Ако трябваше грижливо да пишете код за обработка на грешки на програмен език, който не поддържа изключения, щеше да е необходимо да окръжавате всяко извикване на метод с код за откриване и обработка на грешки, даже ако извиквате един и същ метод няколко пъти. С изключенията слагате всичко в един блок и обработвате всички грешки в него. Това значи че основната програма е много по-лесна за написване и четене, понеже кодът не се преплита с проверките за грешки.
Обработчици на изключения
Разбира се, изхвърленото изключения трябва да завърши някъде. Това “място” е exception handler-а, а има по един за всеки тип изключение което искате да хванете. Обработчиците на изключения непосредствено следват трай блока и са означени с ключовата дума catch:
try {
// Code that might generate exceptions
} catch(Type1 id1) {
// Handle exceptions of Type1
} catch(Type2 id2) {
// Handle exceptions of Type2
} catch(Type3 id3) {
// Handle exceptions of Type3
}
// etc...
Всяка клауза за хващане (exception handler) прилича на малък метод който взема един и само един аргумент от определен тип. Идентификаторът (id1, id2, и т.н.) може да бъде използван вътре в хендлъра, точно като аргумент на метод. Понякога не употребявате идентификатора, понеже типът дава дотатъчно информация, но идентификаторът трябва да си бъде там.
Обработчиците трябва да се появят непосредствено до трай блока. Ако е изхвърлено изключениемеханизмът за изключенията тръгва на лов за първия хендлър който е за дадения тип. После влиза в клаузата за хващане, а изключението се счита обработено. (Търсенето спира щом се влезе в catch клаузата.) Изпълнява се само съвпадащата клауза; не е както при switch оператора където трябва break след всеки case за да не се изпълнят и следващите.
Забележете че, с try блока, различни методи могат да генерират същото изключение, но е необходим само един обработчик.
Прекратяване vs. продължаване
Има два основни модела в теорията на обработката на изключения. В прекратяването (което се поддържа в Java и C++) се предполага, че грешката е толкова критична, че не може нищо да се направи на мястото на възникване. Който е изхвърлил изключението е преценил, че не може да се спаси положението и не иска да се връща.
Алтернативата се нарича продължаване. Това значи че обработчикът на изключения се оставя да свърши нещо за поправяне на ситуацията, а после методът в който е възникнало изключението се повтаря, с предполагаем успех този път. Ако предпочитате продължаване, значи се надявате да може да продължите след обработката на изключението. В този случай вашето изключение е по-подобно на извикване на метод – което и ще направите, за да имитирате тази идеология в Java в случаите когато искате такова поведение. (Тоест, не изхвърляйте изключение; извикайте метод, който да оправи нещата.) Алтернативно, сложете вашия try вътре в while цикъл който продължава да влиза пак в try блока докато резултатът стане удовлетворителен.
Исторически програмистите са използвали нещо подобно на продължаване от операционната система, което в края на краищата е завършвало с прекратяване чрез пропускане на продължаващия код. Така че и да изглежда по-привлекателно на пръв поглед, продължаването изглежда че не е чак толкова полезно на практика. Главната причине е вероятно свързването което се получава: вашият хендлър често трябва да знае каде е възникнало изключението и да съдържа не-родов код за конкретното място. Това прави кода труден за писане и поддържане, особено за големи системи, където изключението може да възникне в различни точки.
Специфициране на изключението
В Java се изисква да информирате клиент-програмиста, който вика вашия метод, за изключенията които биха могли да бъдат изхвърлени от този метод. Това е цивилизовано, понеже потребителят може да узнае какъв точно код да напише за прихващането на всичките възможни изключения. Разбира се, ако резполага със сорса, въпросният програмист може да го разгледа и да намери къде има throw оператори, но често библиотеките не изват със сорс. За да се избегне проблемът, Java дава синтаксис (и изисква да го използвате) за да може да кажете учтиво на клиента какви изключения изхвърля вашия метод, така че да може да бъдат обработени. Това е спецификация на изключенията и е част от декларацията на метода, появяваща се след списъка аргументи.
Спецификацията на изключението използва допълнителна ключова дума, throws, следвана от типовете на потенциалните изключения. Тоест дефиницията на метода би могла да изглежда така:
void f() throws tooBig, tooSmall, divZero { //...
Ако напишем
void f() { // ...
това значи че не се изхвърлят изключения от метода. (Освен от тип RuntimeException, който може да бъде изхвърлен навсякъде – това ще се опише по-нататък.)
Не може да се лъже със спецификацията на изключенията – ако вашият метод предизвиква изключения и не се справя с тях, компилаторът ще открие това и ще ви застави или да поддържате изключението, или да го опишете (споменете) в спецификацията. Чрез налагане на спецификация на изключенията отгоре до долу Java че ще има коректност в изключенията по време на изпълнение.2
Има едно място където може да лъжете: може да твърдите че изхвърляте изключение, а да не го правите. Компилаторът си взема наум вашето и заставя потребителите на метода да поддържат въпросното изключение. Това има полезния ефек да означава изключението само, така че да може да започнете да изхвърляте изключението по-късно, без промяна на съществуващия код.
Хващане на кое да е изключение
Възможно е да се създаде обработчик, който хваща всякакъв тип изключение. Това се прави чрез хващане на базовия тип Exception (има и други видове базови изключения, но Exception е базата която е уместна практически за всички програмни активности):
catch(Exception e) {
System.out.println("caught an exception");
}
Това ще хване всякакво изключение, така че ако ще го използвате ще го сложите накрая на вашия списък от обработчици, за да се избегне отнемането на изключенията на обработчиците, които иначе биха се намирали след него.
Тъй като класът Exception е базов за всичките изключения които са важни за програмиста, не получавате много информация за изключението, но може да викате методите които идват от неговия базов тип Throwable:
String getMessage( )
Взема детайлно съобщение.
String toString( )
Връща кратко описание на Throwable, включително детайлно съобщение ако има такова.
void printStackTrace( )
void printStackTrace(PrintStream)
Извежда Throwable и трасирания стек на извикванията на Throwable. Стекът на извикванията показва веригата от викания на методи, която е довела до мястото, където е изхвърлено изключението.
Първата версия извежда на стандартния изход за грешки, втората на поток по ваш избор. Ако работите под Windows, не можете да пренасочите стандартната грешка и затова може да искате да използвате друг поток и накрая System.out; по този начин изходът може да бъде пренасочен по какъвто вие искате начин.
В добавка получавате някои други методи от базовия тип на Throwable — Object (базовия тип на всичко). Метод който може да е полезен при изключенията е getClass( ), който връща обект, представящ класа на този обект. Може тогава да питате този Class обект за името му чрез getName( ) или toString( ). Може да правите също по-сложни неща с Class обектите които неща не са необходими при обработката на изключения. Class обектите ще се изучават по-късно в тази книга.
Ето пример който показва използването на методите на Exception: (Виж страница 89 при проблеми с пускането на програмата.)
//: c09:ExceptionMethods.java
// Demonstrating the Exception Methods
package c09;
public class ExceptionMethods {
public static void main(String[] args) {
try {
throw new Exception("Here's my Exception");
} catch(Exception e) {
System.out.println("Caught Exception");
System.out.println(
"e.getMessage(): " + e.getMessage());
System.out.println(
"e.toString(): " + e.toString());
System.out.println("e.printStackTrace():");
e.printStackTrace();
}
}
} ///:~
Изведеното е:
Caught Exception
e.getMessage(): Here's my Exception
e.toString(): java.lang.Exception: Here's my Exception
e.printStackTrace():
java.lang.Exception: Here's my Exception
at ExceptionMethods.main
Вижда се, че методите измеждат все повече информация с наследяването – всеки ефективно е надмножество на предишния.
Преизхвърляне на изключение
Понякога ще искате да изхвърлите повторно изключението, което току-що сте хванали, в частност когато сте използвали Exception за хващане на което и да е изключение. Понеже вече имате манипулатор към текущото изключение, може просто да изхвърлите този манипулатор:
catch(Exception e) {
System.out.println("An exception was thrown");
throw e;
}
Преизхвърлянето на изклчението довежда изключението до обработчиците от следващия по-голям контекст. Всякакъв по-нататъшен catch блок за същия try блок се игнорира. Освен това всичко за обекта на изключението се съхранява, така че обработчикът от по-високия контекст разполага с всичката информация за изключението.
Ако просто преизхвърлите текущото изключение, информацията която бихте извели за него в printStackTrace( ) ще отразява оригиналното възникване на изключение, а не мястото, където го преизхвърляте. Ако искате да инсталирате нова трасираща информация за стека, може да го направите чрез извикване на fillInStackTrace( ), което връща обект-изключение чрез наслагването на текущата информация за стека към стария обект. Ето как изглежда:
//: c09:Rethrowing.java
// Demonstrating fillInStackTrace()
public class Rethrowing {
public static void f() throws Exception {
System.out.println(
"originating the exception in f()");
throw new Exception("thrown from f()");
}
public static void g() throws Throwable {
try {
f();
} catch(Exception e) {
System.out.println(
"Inside g(), e.printStackTrace()");
e.printStackTrace();
throw e; // 17
// throw e.fillInStackTrace(); // 18
}
}
public static void
main(String[] args) throws Throwable {
try {
g();
} catch(Exception e) {
System.out.println(
"Caught in main, e.printStackTrace()");
e.printStackTrace();
}
}
} ///:~
Важните номера на редове са дадени в коментар. С ред 17 некоментиран (както е показано), изходът е:
originating the exception in f()
Inside g(), e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:8)
at Rethrowing.g(Rethrowing.java:12)
at Rethrowing.main(Rethrowing.java:24)
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:8)
at Rethrowing.g(Rethrowing.java:12)
at Rethrowing.main(Rethrowing.java:24)
Така че трасирането на стека на изключението винаги помни своето място на появяване, без значение колко пъти е преизхвърляно.
С ред 17 коментиран и ред 18 некоментиран се използва този път fillInStackTrace( ) и резултатът е:
originating the exception in f()
Inside g(), e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.f(Rethrowing.java:8)
at Rethrowing.g(Rethrowing.java:12)
at Rethrowing.main(Rethrowing.java:24)
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
at Rethrowing.g(Rethrowing.java:18)
at Rethrowing.main(Rethrowing.java:24)
Поради fillInStackTrace( ) ред 18 става нова начална точка на изключението.
Класът Throwable трябва да се появи в спецификацията на изключенията на g( ) и main( ) понеже fillInStackTrace( ) произвежда манипулатор към Throwable обект. Понеже Throwable е базов клас на Exception, възможно е да се получи обект който е Throwable но не Exception, така че манипулаторът Exception в main( ) би могъл да го пропусне. За да осигури че всичко е наред компилаторът принуждава към спецификация на изключение Throwable. Например изключението в следната програма не се хваща в main( ):
//: c09:ThrowOut.java
public class ThrowOut {
public static void
main(String[] args) throws Throwable {
try {
throw new Throwable();
} catch(Exception e) {
System.out.println("Caught in main()");
}
}
} ///:~
Възможно е също да се преизхвърли различно от хванатото съобщение. Ако направите това получавате подобен ефект като с използването на fillInStackTrace( ): информацията за оригиналното положение на изхвърлянето е загубена, а имате информацията свързана с новия throw:
//: c09:RethrowNew.java
// Rethrow a different object from the one that
// was caught
public class RethrowNew {
public static void f() throws Exception {
System.out.println(
"originating the exception in f()");
throw new Exception("thrown from f()");
}
public static void main(String[] args) {
try {
f();
} catch(Exception e) {
System.out.println(
"Caught in main, e.printStackTrace()");
e.printStackTrace();
throw new NullPointerException("from main");
}
}
} ///:~
Изходът е:
originating the exception in f()
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
at RethrowNew.f(RethrowNew.java:8)
at RethrowNew.main(RethrowNew.java:13)
java.lang.NullPointerException: from main
at RethrowNew.main(RethrowNew.java:18)
Последното изключение знае само че е възникнало в main( ), а не от f( ). Забележете че Throwable не е непременно в някоя спецификация на изключения.
Никога не се грижим за почистване на предишното изключение, или за каквото и да е изключение. Те са обекти в хийпа създадени с new, така че боклучарят автоматично ги почиства всичките.
Сподели с приятели: |