Освоюємо Java/Винятки
Виняткова ситуація або просто виняток (англ. exception) – це аварійний стан, який відбувається саме під час виконання програми. Прикладом є — ділення на нуль, помилки читання з файлу та мережі тощо. Іншими словами – це помилки які можуть виникнути при виконанні програми. В ряді мов програмування необхідно заздалегідь передбачити можливість тієї чи іншої помилки і передбачити шлях її обробки. В java для цього передбачений спеціальний механізм винятків.
Винятки в java
[ред.]Виняток в java – це об'єкт, який описує виняткову (тобто, помилкову) ситуацію, що відбулась в певному місці коду. Коли така ситуація виникає створюється об'єкт, який передається («вкидається») в метод, в якому виникла помилка. Далі в методі даний виняток може оброблятися, або бути переданий ще кудись для обробки.
Розглянемо для прикладу наступну програму DivZero.java
public class DivZero {
public static void main(String[] args) {
int my = 0;
int medium = 44 / my;
System.out.println("medium=" + medium);
}
}
Як бачимо в програмі присутнє ділення на нуль. При компіляції ми не отримаємо помилок. Проте, після запуску програми, отримаємо наступне:
Exception in thread "main" java.lang.ArithmeticException: / by zero at DivZero.main(DivZero.java:4)
Це так звана траса стеку викликів. Перший рядок означає тип-винятку. Як бачимо тут маємо ArithmeticException з діленням на нуль. java.lang – це пакет класів, який завжди доступний в програмі і, який містить найбільш використовувані класи. Тобто його не потрібно імпортувати. Другий рядок вказує де саме відбулась виняткова ситуація: 4-й рядок у файлі DivZero.java в методі main() класу DivZero. При цьому як бачимо програма завершила своє виконання аварійно. Для уникнення цього існує відповідний механізм обробки винятків, який дозволяє перехопити виняток, одержати інформацію про нього, обробити його здійснивши певні дії для нормального закінчення або продовження виконання програми.
Типи винятків
[ред.]Усі типи винятків є підкласами класу Throwable, який входить в базовий пакет класів Java - java.lang. Тобто він є вершиною ієрархії класів винятків. Його два підкласи Error та Exception утворюють дві основні гілки винятків.
Клас Error з його підкласами - це помилки виконавчого середовища java. І які зазвичай не виникають при нормальній роботі середовища java. Такі винятки зазвичай не можуть бути оброблені в програмі.
Гілка класу Exception - це винятки, які програма повинна вловлювати(catch). Від даного класу та його підкласів можна утворювати власні підкласи. Важливим його підкласом є клас RuntimeException. Винятки даного типу включають такі винятки як ділення на нуль та помилкова індексація масивів.
Актуальну ієрархію класів винятків можна подивитися і уточнити в офіційній документації до JDK[1].
Конструкція try
[ред.]Для обробки виняткових ситуацій використовується п’ять ключових слів: try, catch, throw, throws та finally. Інструкції програми, в яких може виникнути помилка, контролюються за допомогою конструкції try.
Загальна форма наступна:
try{ //блок коду для контролю над помилками } catch (тип-винятку1 об’єкт-винятку) { //дії при виникненні типу винятку1 (обробник винятку) } catch (тип-винятку2 об’єкт-винятку) { //дії при виникненні типу винятку2 (обробник винятку) } //…. [finally{ //дії при виході з конструкції try. }]
Після інструкції try ми розміщуємо «небезпечний» код, у блоці catch відбувається обробка винятку, причому може бути кілька інструкцій catch. При використанні кількох інструкцій catch необхідно зважати на ієрархію класів винятків. Класи нащадки повинні оброблятися раніше за батьківські класи. Інакше обробка конкретного винятку просто буде неможлива, оскільки першим буде викликаний catch з батьківським класом (див. поліморфізм) і до нащадків діло не дійде. Завершувати конструкцію може інструкція finally, в ній розміщується код, який буде виконаний або після завершення коду в конструкції try або після обробки винятку в інструкції catch.
Починаючи з java 7 стало можливим обробити два різнотипні винятки в одному блоці catch:
catch (FileNotFoundException | UnknownHostException е) { // дії при обробці винятків }
Так слід робити, якщо винятки не є підкласами один для іншого.
Таким чином можемо переписати програму з діленням на нуль:
public class DivZero {
public static void main(String args[]){
int my=0;
try{
int medium=44/my;
System.out.println("medium="+medium);
}catch(ArithmeticException e){
System.out.println("Ділення на нуль!");
}
System.out.println("Продовження виконання...");
}
}
Результат виконання:
Ділення на нуль! Продовження виконання...
Як бачимо, для перехоплення винятку, код, через який виникала помилка, знаходиться у середині конструкції try. Також, зверніть увагу, що після виникнення виняткової ситуації наступний рядок System.out.println("medium="+medium); не було виведено, оскільки виняток був переданий для обробки в catch. Після інструкції програми в якій відбулась виняткова ситуація, всі наступні рядки до інструкції catch пропускаються і не будуть виконуватись. І, як бачимо з результату виконання, наша програма продовжила виконання по закінченню блоку try, а не завершила своє виконання аварійно.
Конструкція try може бути вкладеною в іншу конструкцію try.
try з ресурсами
[ред.]При використанні всередині try певних ресурсів, наприклад, файлів, при виникненні винятку необхідно було передбачити закриття відкритих ресурсів. Для цієї мети раніше приходилося використовувати блок finally. У Java 7 з'явилася конструкція try з ресурсами (англ. try-with-resources)[2]. Тепер просто можна створити ресурс у дужках зразу ж після ключового слова try і java сама потурбується про закриття ресурсу. Приклад:
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
В конструкції try з ресурсами можна вказати декілька ресурсів:
try (var in = new Scanner(new FileinputStream
("/usr/share/dict/words"), StandardCharsets.UTF_8);
var out = new PrintWriter("out.txt", StandardCharsets.UTF_8))
{
//код, що використовує ресурси
}
Throw
[ред.]В наведених вище прикладах ми здійснювали лише обробку винятків викинутих виконавчим середовищем java. Проте існує можливість викидання власних винятків. Для цього існує інструкція Throw. Загальна форма її наступна:
throw ThrowableInstance;
Тут ThrowableInstance – це тип винятку, який повинен бути або типом Throwable, або мати тип його підкласів. Щоб програма викинула ваш виняток необхідно скористатися оператором new. Для того, щоб одержати(перехопити) тип винятку можна скористатися інструкцією catch, як це ми робили вище.
Після інструкції throw відбувається аналогічне породження винятку як у вище наведених прикладах. Тобто усі інструкції пропускаються до найближчого блоку інструкції catch, де необхідно здійснити обробку винятку. Якщо catch не буде знайдено, то обробник винятків, що використовується по замовчуванню, призупиняє виконання програми і друкує відбиток стеку (stack trace).
Приклад:
package exceptions;
public class ThrowNullException {
public static void main(String args[]){
try{
throw new NullPointerException("Пробний виняток");
}catch(Exception e){
System.out.println("Наш витяток: "+e);
}
}
}
Результат:
Наш витяток: java.lang.NullPointerException: Пробний виняток
Програма демонструє як створювати один із стандартних винятків. Більшість вбудованих run-time винятків Java мають щонайменше два конструктори. Один за замовчуванням без параметрів і один із параметром String, який дозволяє задати додатковий опис. Опис можна вивести на консоль за допомогою методів print(), println(). Також його можна отримати використавши метод getMessage() класу Throwable. Тобто написати так:
System.out.println(e.getMessage());
В результаті отримаємо на консолі рядок:
Пробний виняток
Замість використання методів print() та println() можна написати e.printStackTrace() і отримати відбиток стеку(трасування стеку). Так якщо ми в наведеному виклику в catch використаємо e.printStackTrace():
}catch(Exception e){
e.printStackTrace();
}
то ми отримаємо в консолі:
java.lang.NullPointerException: Пробний виняток at exceptions.ThrowNullException.main(ThrowNullException.java:6)
Можна створити власний тип винятку як підклас уже існуючого типу.
Створення власних винятків
[ред.]Якщо вам не підходить жоден з існуючих класів винятків, то можна створити власний. Він повинен розширювати клас Exception, або будь-який із похідних класів.
package exceptions;
import java.io.IOException;
class FileFormatException extends IOException
{
public FileFormatException () {} // конструктор по замовчуванню
public FileFormatException(String gripe) // конструктор, що забезпечує повернення зазначеного повідомлення
{
super(gripe);
}
}
Тепер в коді, що повинен породжувати виняток можна вставити викидання даного винятку:throw new FileFormatException();
.
package exceptions;
public class TestNewException {
public static void main(String[] args) {
try {
throw new FileFormatException("Не той формат файлу");
} catch (Exception e) {
System.out.println("Повідомлення винятку: "+e.getMessage());
}
}
}
Результат:
Повідомлення винятку: Не той формат файлу
Throws
[ред.]Якщо метод породжує виняток і не обробляє його, то він повинен вказати про це, щоб обробка винятку була здійснена у місці виклику даного методу. Це здійснюється за допомогою застереження throws в оголошенні методу. Після нього вказується підряд через кому усі винятки, які можуть бути викинуті методом, окрім винятків класів Error та RuntimeException і їхніх підкласів. Нагадаємо, що клас Error – це необроблювані винятки, RuntimeException – винятки, які виникають в результаті помилки програміста (вихід за межі масиву, нульове посилання, невірне перетворення типів). Інші винятки – це помилки доступу, які доволі часто вимагають відповідної обробки.
Загальна форма оголошення методу наступна:
тип ім’я_методу(список_параметрів) throws список_винятків { // тіло методу }
Наступна програма демонструє використання throws у методі де виникає виняток IllegalAccessException.
public class ThrowsException {
public static void exceptionMethod () throws IllegalAccessException{
System.out.println("Всередині exceptionMethod().");
throw new IllegalAccessException("Помилка доступу");
}
public static void main(String args[]){
try{
exceptionMethod();
System.out.println("Кінець програми"); //даний рядок не буде виведений
}catch(IllegalAccessException e){
System.out.println("Наш витяток: "+e);
}
}
}
Результат:
Всередині ExceptionMethod(). Наш витяток: java.lang.IllegalAccessException: Помилка доступу
Як бачимо тепер обробка винятку відбувається у методі main(). Без інструкції try-catch програма призупинятиметься з друком відбитку стеку. Слід зауважити, що у методах інструкція throw поводить себе подібно до інструкції return. Тобто виконання методу припиняється і відбувається повернення в місце виклику методу.
Інструкцію throws не можливо використати при заміщенні методу в класах нащадках, якщо аналогічний метод батьківського класу не викидав виняток.
Зчеплення винятків
[ред.]У java починаючи з версії jdk 1.4, тобто доволі вже давно, були введені зчеплені винятки (англ. chained exceptions). Їх можна, наприклад, використати коли у вас виникає ділення на нуль після вводу певного значення. В результаті виникає виняток ArithmeticException. Зрозуміло, що основною причиною є те, що з консолі (чи іншим чином) було введено 0, тобто основною причиною є помилка вводу і краще вказати виняток IOException. Тому ми можемо приєднати додаткову інформацію до того винятку, що був викинутий програмою.
Створити зчеплення винятків можна кількома способами.
Перший спосіб, це використання відповідних конструкторів:
// Конструктор створює виняток із зчепленим причиною-винятком Throwable(Throwable причина_винятку) // Конструктор створює виняток із описом винятку та із зчепленим причиною-винятком Throwable(String опис_винятку, Throwable причина_винятку)
Також можна зчепити причину-виняток з існуючим винятком за допомогою метода initCause():
Throwable initCause(Throwable причина_винятку)
Щоб отримати доступ до причини-винятку, використовують метод getCause():
Throwable getCause()
Згаданий випадок, з діленням на нуль, демонструє наступна програма:
package exceptions;
import java.io.IOException;
import java.util.Scanner;
public class TestChainedExceptions {
static public void doOperation() {
//використовуємо try з ресурсами
try (Scanner in = new Scanner(System.in);) {
System.out.print("Введіть дільник: ");
int divisor = in.nextInt();
int medium = 44 / divisor; //якщо 0, то викине виняток ArithmeticException
System.out.println("medium=" + medium);
}
catch (ArithmeticException e){
e.initCause(new IOException("Помилка вводу даних"));
throw(e); //викинути виняток знову із додатковою інформацією
//виняток буде вловлений в методі main()
}
}
public static void main(String[] args) {
try{
doOperation();
}catch(Exception e) {
System.out.println("Основний виняток: "+e);
System.out.println("Причина винятку: "+e.getCause());
System.out.println("Відбиток стеку:");
e.printStackTrace();
}
}
}
Результат виконання:
Введіть дільник: 0
Основний виняток: java.lang.ArithmeticException: / by zero
Причина винятку: java.io.IOException: Помилка вводу даних
Відбиток стеку:
java.lang.ArithmeticException: / by zero
at exceptions.TestChainedExceptions.doOperation(TestChainedExceptions.java:12)
at exceptions.TestChainedExceptions.main(TestChainedExceptions.java:23)
Caused by: java.io.IOException: Помилка вводу даних
at exceptions.TestChainedExceptions.doOperation(TestChainedExceptions.java:16)
... 1 more
Додаткові джерела
[ред.]Примітки
[ред.]- ↑ Java™ Platform, Standard Edition 7 API Specification (Натисніть TREE для перегляду ієрархії класів)
- ↑ The try-with-resources Statement