JavaRush /Java блог /Random UA /Винятки в Java: перехоплення та обробка

Винятки в Java: перехоплення та обробка

Стаття з групи Random UA
Вітання! Не хочеться тобі про це говорити, але величезна частина роботи програміста це робота з помилками. Причому найчастіше зі своїми власними. Так уже склалося, що не буває людей, які не припускаються помилок. І таких програм теж не буває. Звичайно, головне під час роботи над помилкою — зрозуміти її причину. А причин таких у програмі може бути ціла купа. Одного разу перед творцями Java постало питання: що робити з цими потенційними помилками в програмах? Уникнути їх повністю – неможливо. Програмісти можуть понаписати такого, що неможливо навіть уявити:) Отже, треба закласти в мову механізм роботи з помилками. Іншими словами, якщо вже у програмі сталася якась помилка, потрібний сценарій для подальшої роботи. Що саме програма має робити у разі виникнення помилки? Сьогодні ми познайомимося із цим механізмом. І називається він "Виключення" (Exceptions ).

Що таке виняток у Java

Виняток — якась виняткова, незапланована ситуація, що сталася під час роботи програми. Прикладів винятків Java може бути багато. Наприклад, ти написав код, який зчитує текст із файлу і виводить у консоль перший рядок.
public class Main {

   public static void main(String[] args) throws IOException {
       BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
       String firstString = reader.readLine();
       System.out.println(firstString);
   }
}
Але такого файлу немає! Результатом роботи програми буде виняток FileNotFoundException. Висновок:

Exception in thread "main" java.io.FileNotFoundException: C:\Users\Username\Desktop\test.txt (Системе не удается найти указанный путь)
Кожен виняток представлений у Java окремим класом. Усі класи винятків походять від загального предка - батьківського класу Throwable. Назва класу-виключення зазвичай коротко відображає причину його виникнення:
  • FileNotFoundException(Файл не знайдено)
  • ArithmeticException(Виняток при виконанні математичної операції)
  • ArrayIndexOutOfBoundsException(зазначений номер осередку масиву за межами його довжини). Наприклад, якщо спробувати вивести в консоль комірку array[23] масиву array довжиною 10.
Усього таких класів у Java майже 400 штук! Навіщо так багато? Саме для того, щоб програмістам було зручніше працювати з ними. Уяви собі: ти написав програму, і вона при роботі видає виняток, який має такий вигляд:
Exception in thread "main"
Е-е-е-м :/ Нічого не зрозуміло. Що за помилка, звідки взялася — неясно. Жодної корисної інформації немає. А ось завдяки такій різноманітності класів програміст отримує для себе головне — тип помилки та її ймовірну причину, яка закладена у назві класу. Адже зовсім інша справа побачити у консолі:
Exception in thread "main" java.io.FileNotFoundException: C:\Users\Username\Desktop\test.txt (Системе не удается найти указанный путь)
Відразу стає зрозуміло, в чому може бути справа і "у який бік копати" для вирішення проблеми! Винятки, як будь-які екземпляри класів, є об'єктами.

Перехоплення та обробка винятків

Для роботи з винятками Java існують спеціальні блоки коду: try, catchі finally. Винятки: перехоплення та обробка - 2Код, у якому програміст очікує виникнення винятків, міститься у блок try. Це не означає, що виняток тут обов'язково відбудеться. Це означає, що воно може там статися, і програміст знає про це. Тип помилки, який ти очікуєш отримати, міститься в блок catch(перехоплення). Сюди міститься весь код, який потрібно виконати, якщо виняток відбудеться. Ось приклад:
public static void main(String[] args) throws IOException {
   try {
       BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));

       String firstString = reader.readLine();
       System.out.println(firstString);
   } catch (FileNotFoundException e) {

       System.out.println("Помилка! Файл не знайдено!");
   }
}
Висновок:

Ошибка! Файл не найден!
Ми помістабо наш код у два блоки. У першому блоці ми очікуємо, що може статися помилка "Файл не знайдено". Це блок try. У другому - вказуємо програмі що робити, якщо сталася помилка. Причому помилка конкретного виду - FileNotFoundException. Якщо ми передамо в дужки блоку catchінший клас виключення, його не буде перехоплено.
public static void main(String[] args) throws IOException {
   try {
       BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));
       String firstString = reader.readLine();
       System.out.println(firstString);
   } catch (ArithmeticException e) {

       System.out.println("Помилка! Файл не знайдено!");
   }
}
Висновок:

Exception in thread "main" java.io.FileNotFoundException: C:\Users\Username\Desktop\test.txt (Системе не удается найти указанный путь)
Код у блоці catchне відпрацював, тому що ми "налаштували" цей блок на перехоплення ArithmeticException, а код у блоці tryвикинув інший тип - FileNotFoundException. Для FileNotFoundExceptionнас не написали сценарій, тому програма вивела в консоль ту інформацію, яка виводиться за умовчанням для FileNotFoundException. Тут потрібно звернути увагу на 3 речі. Перше. Як тільки в якомусь рядку коду в блоці try виникне виняток, код після неї вже не буде виконано. Виконання програми відразу "перестрибне" в блок catch. Наприклад:
public static void main(String[] args) {
   try {
       System.out.println("Ділімо число на нуль");
       System.out.println(366/0);//у цьому рядку коду буде викинуто виняток

       System.out.println("Цей");
       System.out.println("код");
       System.out.println("ні");
       System.out.println("буде");
       System.out.println("виконаний!");

   } catch (ArithmeticException e) {

       System.out.println("Програма перестрибнула в блок catch!");
       System.out.println("Помилка! Не можна ділити на нуль!");
   }
}
Висновок:

Делим число на ноль 
Программа перепрыгнула в блок catch! 
Ошибка! Нельзя делить на ноль! 
У блоці tryу другому рядку ми спробували розділити число на 0, у результаті виник виняток ArithmeticException. Після цього рядки 6-10 блоки tryвиконані вже не будуть. Як ми й казали, програма відразу почала виконувати блок catch. Друге. Блоків catchможе бути кілька. Якщо код у блоці tryможе викинути не один, а кілька видів винятків, для кожного можна написати свій блок catch.
public static void main(String[] args) throws IOException {
   try {
       BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));

       System.out.println(366/0);
       String firstString = reader.readLine();
       System.out.println(firstString);
   } catch (FileNotFoundException e) {

       System.out.println("Помилка! Файл не знайдено!");

   } catch (ArithmeticException e) {

       System.out.println("Помилка! Розподіл на 0!");

   }
}
У цьому прикладі ми написали два блоки catch. Якщо у блоці tryвідбудеться FileNotFoundException, буде виконано перший блок catch. Якщо станеться ArithmeticException, виконається другий. Блоків catchти можеш написати хоч 50. Але, звісно, ​​краще не писати код, який може викинути 50 різних видів помилок. Третє. Звідки тобі знати, які винятки може викинути код? Ну, про деякі ти, звичайно, можеш здогадуватися, але пам'ятати все неможливо. Тому компілятор Java знає про найпоширеніші винятки і знає, в яких ситуаціях вони можуть виникнути. Наприклад, якщо ти написав код і компілятор знає, що під час його роботи можуть виникнути 2 види винятків, твій код не скомпілюється, доки ти їх не обробиш. Приклади цього ми побачимо нижче. Тепер щодо обробки винятків. Існує 2 способи їх обробки. З першим ми вже познайомабося - метод може обробити виняток самостійно в блоці catch(). Є й другий варіант – метод може викинути виняток нагору по стеку викликів. Що це означає? Наприклад, у нас у класі є метод — той самийprintFirstString(), який зчитує файл і виводить у консоль його перший рядок:
public static void printFirstString(String filePath) {

   BufferedReader reader = new BufferedReader(new FileReader(filePath));
   String firstString = reader.readLine();
   System.out.println(firstString);
}
На сьогоднішній день наш код не компілюється, тому що в ньому є необроблені винятки. У рядку 1 вказуєш шлях до файлу. Компілятор знає, що такий код може призвести до FileNotFoundException. У рядку 3 ти читаєш текст із файлу. У цьому процесі легко може виникнути IOExceptionпомилка при вводі-виведення даних (Input-Output). Зараз компілятор каже тобі: “Чувак, я не схвалю цей код і не скомпілюю його, поки ти не скажеш мені, що я маю робити у випадку, якщо станеться одне з цих винятків. А вони можуть статися, виходячи з того коду, який ти написав!” . Подітися нікуди, потрібно обробляти обидва! Перший варіант обробки нам уже знайомий: треба помістити наш код у блок try, і додати два блоки catch:
public static void printFirstString(String filePath) {

   try {
       BufferedReader reader = new BufferedReader(new FileReader(filePath));
       String firstString = reader.readLine();
       System.out.println(firstString);
   } catch (FileNotFoundException e) {
       System.out.println("Помилка, файл не знайдено!");
       e.printStackTrace();
   } catch (IOException e) {
       System.out.println("Помилка введення/виведення даних із файлу!");
       e.printStackTrace();
   }
}
Але це єдиний варіант. Ми можемо не писати сценарій для помилки всередині методу, і просто прокинути виняток. Це робиться за допомогою ключового слова throws, яке пишеться в оголошенні методу:
public static void printFirstString(String filePath) throws FileNotFoundException, IOException {
   BufferedReader reader = new BufferedReader(new FileReader(filePath));
   String firstString = reader.readLine();
   System.out.println(firstString);
}
Після слова throwsми через кому перераховуємо всі види винятків, які цей метод може викинути під час роботи. Навіщо це робиться? Тепер, якщо хтось у програмі захоче викликати метод printFirstString(), він повинен сам реалізувати обробку винятків. Наприклад, в іншій частині програми хтось із твоїх колег написав метод, усередині якого викликає твій метод printFirstString().
public static void yourColleagueMethod() {

   //...метод твого колеги щось робить

   //...і одночасно викликає твій метод printFirstString() c потрібним йому файлом
   printFirstString("C:\\Users\\Євген\\Desktop\\testFile.txt");
}
Помилка код не компілюється! У методі printFirstString()ми не написали сценарію обробки помилок. Тому завдання лягає на плечі тих, хто використовуватиме цей метод. Тобто перед методом yourColleagueMethod()тепер стоять ті ж 2 варіанти: він повинен або обробити обидва винятки, які йому "прилетіли", за допомогою try-catchабо прокинути їх далі.
public static void yourColleagueMethod() throws FileNotFoundException, IOException {
   //...метод щось робить

   //...і одночасно викликає твій метод printFirstString() c потрібним йому файлом
   printFirstString("C:\\Users\\Євген\\Desktop\\testFile.txt");
}
У другому випадку обробка ляже на плечі наступного за стеком методу - того, який викликатиме yourColleagueMethod(). Саме тому такий механізм називається "прокиданням виключення нагору", або "передачею нагору". Коли ти прокидаєш винятки нагору за допомогою throws, код компілюється. Компілятор в цей момент ніби каже: Окей, гаразд. Твій код містить купу потенційних винятків, але я, так і бути, його скомпілюю. Ми ще повернемося до цієї розмови!” І коли ти десь у програмі викликаєш метод, який не обробив своїх винятків, компілятор виконує свою обіцянку і знову нагадує про них. На завершення ми поговоримо про блок finally(вибачте за каламбур). Це остання частина тріумвірату обробки винятківtry-catch-finally. Його особливість у тому, що він виконується за будь-якого сценарію роботи програми.
public static void main(String[] args) throws IOException {
   try {
       BufferedReader reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));

       String firstString = reader.readLine();
       System.out.println(firstString);
   } catch (FileNotFoundException e) {
       System.out.println("Помилка! Файл не знайдено!");
       e.printStackTrace();
   } finally {
       System.out.println("А ось і блок finally!");
   }
}
У цьому прикладі код усередині блоку finallyвиконується в обох випадках. Якщо код у блоці tryвиконається цілком і не викине винятки, наприкінці спрацює блок finally. Якщо код всередині tryперерветься, і програма перестрибне в блок catchпісля того, як відпрацює код всередині catch, все одно буде обраний блок finally. Навіщо він потрібен? Його головне призначення – виконати обов'язкову частину коду; ту частину, яка має бути виконана незалежно від обставин. Наприклад, у ньому часто звільняють якісь ресурси, що використовуються програмою. У нашому коді ми відкриваємо потік для читання інформації з файлу та передаємо його в об'єкт BufferedReader. НашreaderНеобхідно закрити та звільнити ресурси. Це потрібно зробити у будь-якому випадку: неважливо, відпрацює програма як слід або викличе виняток. Це зручно робити в блоці finally:
public static void main(String[] args) throws IOException {

   BufferedReader reader = null;
   try {
       reader = new BufferedReader(new FileReader("C:\\Users\\Username\\Desktop\\test.txt"));

       String firstString = reader.readLine();
       System.out.println(firstString);
   } catch (FileNotFoundException e) {
       e.printStackTrace();
   } finally {
       System.out.println("А ось і блок finally!");
       if (reader != null) {
           reader.close();
       }
   }
}
Тепер ми точно впевнені, що подбали про зайняті ресурси незалежно від того, що станеться під час роботи програми :) Це ще не все, що потрібно знати про винятки. Обробка помилок – дуже важлива тема у програмуванні: їй присвячена не одна стаття. На наступному занятті ми дізнаємося, які бувають види винятків і як створити власний виняток:) До зустрічі!
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ