Винятки в Java, Частина II (checked / unchecked)

    Це друга частина статті (перша частина — try-catch-finally ), присвяченій такому мовною механізму Java як винятки. Вона має вступний характер і розрахована на початківців розробників або тих, хто тільки приступає до вивчення мови.
 
 1. Магія checked / unchecked
 2. Песимістичний механізм
 3. throws з непроверяемим (unckecked) виключенням
 4. Множинні винятку
 5. Або catch, або throws
 6. Поведінка компілятора / JVM
 7. Overriding і throws
 8. Передача властивості у спадок
 
 
 

1. «Магія» checked / unchecked

 
Механізм виняткових ситуація в Java пов'язаний з двома елементами «магії», тобто поведінки, яке ніяк не відображено у вихідному коді:
1. «Магію» java.lang.Throwable — в throw, catch і throws можуть стояти виключно Throwable або його спадкоємці (ми вже розбирали в попередній лекції ). Це «право» знаходитися в throw, catch і throws ніяк не відображено у вихідному коді.
2. Всі виняткові ситуації діляться на «перевіряються» (checked) і «непроверяемие» (unchecked). Це властивість властива «кореневищу» (Throwable, Error, Exception, RuntimeException) і передається у спадок. Ніяк не видимо у вихідному коді класу винятку.
 
У подальших прикладу просто врахуйте, що
 - Throwable і Exception і всі їх спадкоємці (за винятком спадкоємців Error-а і RuntimeException-а) — checked
 - Error і RuntimeException і всі їх спадкоємці — unchecked
 
checked exception = проверяемое виняток, проверяемое компілятором.
Що саме перевіряє компілятор ми розберемо на цій лекції.
 
Нагадаємо ієрархію винятків
 
Object
                      |
                  Throwable
                  /      \
              Error     Exception
                            |
                    RuntimeException

 
Розставимо значення властивості checked / unchecked
 
Object
                      |
               Throwable(CHECKED)
               /            \
     Error(UNCHECKED)    Exception(CHECKED)
                            |
                  RuntimeException(UNCHECKED)

 
 
 

2. Песимістичний механізм

 
Я називаю зв'язок між перевіряються винятками і throws — «песимістичною», польку ми можемо «налякати» про більше, ніж може відбутися насправді, але не навпаки
 
Ми не можемо кидати, але не попередити
 
public class App {
    public static void main(String[] args) {
        throw new Exception(); // тут ошибка компиляции
    }
}

>> COMPILATION ERROR: unhandled exception: java.lang.Exception

 
Ми не можемо кидати, але попередити про «меншому»
 
import java.io.IOException;

public class App {
    public static void main(String[] args) throws IOException {
        throw new Exception(); // тут ошибка компиляции
    }
}

>> COMPILATION ERROR: unhandled exception: java.lang.Exception

 
Ми можемо попередити точно про те, що кидаємо
 
public class App {
    public static void main(String[] args) throws Exception { // предупреждаем о Exception
        throw new Exception(); // и кидаем Exception
    }
}

 
Ми можемо попередити про більше, ніж ми кидаємо
 
public class App {
    public static void main(String[] args) throws Throwable { // предупреждаем "целом" Throwable
        throw new Exception(); // а кидаем только Exception
    }
}

 
Можемо навіть попередити про те, чого взагалі немає
 
public class App {
    public static void main(String[] args) throws Exception { // пугаем
        // но ничего не бросаем
    }
}

 
Навіть якщо попереджаємо про те, чого немає — всі зобов'язані боятися
 
public class App {
    public static void main(String[] args) {
        f(); // тут ошибка компиляции
    }

    public static void f() throws Exception {
    }    
}

>> COMPILATION ERROR: unhandled exception: java.lang.Exception

 
Хоча вони (злякалися) можуть пропагують інших ще більше
 
public class App {
    // они пугают целым Throwable
    public static void main(String[] args) throws Throwable { 
        f();
    }
    // хотя мы пугали всего-лишь Exception
    public static void f() throws Exception {
    }    
}

 
У чому мета «пессимистичности»? Все досить просто.
Ви в режимі протіпірованія «накидали», скажімо, клас-утиліту для скачування з інтернету
 
public class InternetDownloader {
    public static byte[] (String url) throws IOException { 
        return "<html><body>Nothing! It's stub!</body></html>".getBytes();
    }
}

і хотіли б «примусити» користувачів вашого класу ВЖЕ ОБРОБЛЯТИ можливе виключення IOException, хоча з реалізації-пустушки ви ПОКИ НЕ генеруєте таке виключення. Але в майбутньому — збираєтеся.
  
 
 

3. throws з непроверяемим (unckecked) виключенням

Виклик методу, який «лякає» unchecked винятком не накладає на нас ніяких обов'язків.
 
public class App {
    public static void main(String[] args) { 
        f();
    }
    public static void f() throws RuntimeException {
    }    
}

Ця конструкція служить меті «вказати» програмісту-читачеві коду на те, що ваш метод може викинути деякий непроверяемую (unchecked) виняток.
 
Приклад (java.lang.NumberFormatException — непроверяемую виняток):
 
package java.lang;

public final class Integer extends Number implements Comparable<Integer> {
    ...
    /**
     * ...
     *
     * @param s    a {@code String} containing the {@code int}
     *             representation to be parsed
     * @return     the integer value represented by the argument in decimal.
     * @exception  NumberFormatException  if the string does not contain a
     *               parsable integer.
     */
    public static int parseInt(String s) throws NumberFormatException {
        return parseInt(s,10);
    }
    ...
}

Integer.parseInt () може кинути unchecked NumberFormatException на невідповідному аргументі (int k = Integer.parseInt («123abc»)), це відобразили
 - В сигнатурі методу: throws NumberFormatException
 - В документації (javadoc): @ exception
але це ні до чого нас не зобов'язує.
 
 
 

4. Множинні винятку

Розглянемо ситуацію з кодом, який може кидати перевіряються виключення різних типів.
Далі враховуйте, що EOFException і FileNotFoundException — нащадки IOException.
 
Ми можемо точно вказати, що викидаємо
 
import java.io.EOFException;
import java.io.FileNotFoundException;

public class App {
    // пугаем ОБОИМИ исключениями
    public static void main(String[] args) throws EOFException, FileNotFoundException {
        if (System.currentTimeMillis() % 2 == 0) {
            throw new EOFException();
        } else {
            throw new FileNotFoundException();
        }
    }
}

або ось так
 
import java.io.EOFException;
import java.io.FileNotFoundException;

public class App {
    // пугаем ОБОИМИ исключениями
    public static void main(String[] args) throws EOFException, FileNotFoundException {
        f0();
        f1();
    }
    public static void f0() throws EOFException {...}
    public static void f1() throws FileNotFoundException {...}
}

 
А можемо «налякати» сильніше (предком обох винятків)
 
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;

public class App {
    // пугаем ПРЕДКОМ исключений
    public static void main(String[] args) throws IOException {
        if (System.currentTimeMillis() % 2 == 0) {
            throw new EOFException();
        } else {
            throw new FileNotFoundException();
        }
    }
}

або ось так
 
import java.io.EOFException;
import java.io.FileNotFoundException;

public class App {
    // пугаем ПРЕДКОМ исключений
    public static void main(String[] args) throws IOException {
        f0();
        f1();
    }
    public static void f0() throws EOFException {...}
    public static void f1() throws FileNotFoundException {...}
}

 
Можна і ось так: EOFException і FileNotFoundException «узагальнюємо до» IOException, а InterruptedException «пропускаємо недоторканим» (InterruptedException — НЕ нащадок IOException)
 
import java.io.EOFException;
import java.io.FileNotFoundException;

public class App {
    public static void main(String[] args) throws IOException, InterruptedException {
        f0();
        f1();
        f2();
    }
    public static void f0() throws EOFException {...}
    public static void f1() throws FileNotFoundException {...}
    public static void f2() throws InterruptedException {...}
}

 
 
 

5. Або catch, або throws

Не треба лякати тим, що ви перехопили
так
 
public class App {
    public static void main(String[] args) {
        try {
            throw new Exception();
        } catch (Exception e) {
            // ...
        }
    }
}

 
або так (ставимо catch по предку і точно перехоплюємо)
 
public class App {
    public static void main(String[] args) {
        try {
            throw new Exception();
        } catch (Throwable e) {
            // ...
        }
    }
}

 
Але якщо перехопили тільки нащадка, то чи не вийде
 
public class App {
    public static void main(String[] args) {
        try {
            throw new Throwable();
        } catch (Exception e) {
            // ...
        }
    }
}

>> COMPILATION ERROR: unhandled exception: java.lang.Throwable

 
Не годиться, природно, і перехоплювання «брата»
 
public class App {
    public static void main(String[] args) {
        try {
            throw new Exception();
        } catch (Error e) {
            // ...
        }
    }
}

>> COMPILATION ERROR: unhandled exception: java.lang.Exception

 
Якщо ви частину перехопили, то можете цим не лякати
 
import java.io.EOFException;
import java.io.FileNotFoundException;

public class App {
    // EOFException перехватили catch-ом, им не пугаем
    public static void main(String[] args) throws FileNotFoundException {
        try {
            if (System.currentTimeMillis() % 2 == 0) {
                throw new EOFException();
            } else {
                throw new FileNotFoundException();
            }
        } catch (EOFException e) {
            // ...
        }
    }
}

 
 
 

6. Поведінка компілятора / JVM

Необхідно розуміти, що
 - перевірка на cheched виключення відбувається в момент компіляції (compile-time checking)
 - перехоплення виключень (catch) відбувається в момент виконання (runtime checking)
 
public class App {
    // пугаем Exception
    public static void main(String[] args) throws Exception { 
        Throwable t = new Exception(); // и лететь будет Exception
        throw t; // но тут ошибка компиляции 
    }  
}

>> COMPILATION ERROR: unhandled exception: java.lang.Throwable

 
Повна аналогія з
 
public class App {
    public static void main(String[] args) throws Exception { 
        Object ref = "Hello!";  // ref указывает на строку 
        char c = ref.charAt(0); // но тут ошибка компиляции 
    }  
}

>> COMPILATION ERROR: Cannot resolve method 'charAt(int)'

Хоча ПОСИЛАННЯ ref ВКАЗУЄ на об'єкт типу java.lang.String (у якого є метод charAt (int)), але ТИП ПОСИЛАННЯ — java.lang.Object (посилання типу java.lang.Object на об'єкт типу java.lang.String). Компілятор орієнтується на «лівий тип» (тип посилання, а не тип засиланого) і не пропускає такий код.
 
Хоча У ЦІЙ СИТУАЦІЇ компілятор міг би розібрати, що t посилається на Exception, а ref — на String, але цього вже неможливо зробити при роздільно компіляції. Уявіть, що ми МОГЛИ Б скомпілювати ОКРЕМО такий клас, упакувати в jar і поширювати
 
// НЕ КОМПИЛИРУЕТСЯ! ИССЛЕДУЕМ ГИПОТЕТИЧЕСКУЮ СИТУАЦИЮ!
public class App {
    public static void f0(Throwable t) throws Exception { 
        throw t;
    }  
    public static void f1(Object ref){ 
        char c = ref.charAt(0);
    }  
}


А хтось бере цей клас, додає в classpath і викликає App.f0 (new Throwable ()) або App.f1 (new Integer (42)). У такому випадку JVM зіткнулася б з ситуацією, коли від неї вимагає кинути проверяемое виняток, яка не відстежив компілятор (у випадку з f0) або викликати метод, якого немає (у випадку з f1)!
 
Java — мова зі статичної типізацією (тобто відстеження коректності використання типів (наявність використовуваних полів, наявність викликаються методів, перевірка на checked винятку, ...) проводиться компілятором), забороняє таку поведінку. У деяких мовах (мови з динамічною типізацією — відстеження коректності використання типів проводиться середовищем виконання (воно дозволено, наприклад в JavaScript).
 
Компілятор не пропустить цей код, хоча метод main ГАРАНТОВАНО не викинуть ВИНЯТКИ
 
public class App {
    // пугаем Exception
    public static void main(String[] args) throws Exception { 
        try {
            Throwable t = new Exception(); // и лететь будет Exception
            throw t; // но тут ошибка компиляции 
        } catch (Exception e) {
            System.out.println("Перехвачено!");
        }
    }  
}

>> COMPILATION ERROR: unhandled exception: java.lang.Throwable

 
 
public class App {
    // ТЕПЕРЬ пугаем Throwable
    public static void main(String[] args) throws Throwable { 
        try {
            Throwable t = new Exception(); // а лететь будет Exception
            throw t;
        } catch (Exception e) { // и мы перехватим Exception
            System.out.println("Перехвачено!");
        }
    }  
}

>> Перехвачено!

 
 
 

7. Overriding і throws

При перевизначенні (overriding) список винятків нащадка не зобов'язаний співпадати з таким у предка.
Але він має бути «не сильніше» списку предка:
 
import java.io.FileNotFoundException;
import java.io.IOException;

public class Parent {
    // предок пугает IOException и InterruptedException
    public void f() throws IOException, InterruptedException {}
}

class Child extends Parent {
    // а потомок пугает только потомком IOException
    @Override
    public void f() throws FileNotFoundException {}
}

 
Однак тут ми спробували «розширити тип» киданих винятків
 
import java.io.IOException;

public class Parent {
    public void f() throws IOException, InterruptedException {}
}

class ChildB extends Parent {
    @Override
    public void f() throws Exception {}
}

>> COMPILATION ERROR: overridden method does not throw 'java.lang.Exception'

 
Чому можна звужувати тип, але не розширювати?
Розглянемо наступну ситуацію:
 
public class Parent {
    // предок пугает Exception
    public void f() throws Exception {}
}

 
 
// тут ошибка компиляции в Java, но, ДОПУСТИМ, ее нет
public class Child extends Parent {
    // потомок расширил Exception до Throwable
    public void f() throws Throwable {}
}

 
 
public class Demo {
    public static void test(Parent ref) {
        // тут все компилируется, Parent.f() пугает Exception и мы его ловим catch
        try {
            ref.f();
        } catch(Exception e) {}
    }
}

 
 
public class App {
    public static void main(String[] args) {
        // тут все компилируется, Demo.test хотел Parent и мы дали ему подтип - Child
        Demo.test(new Child());
    }
}

 
Уважно подивіться на цей приклад — якби нащадок міг розширювати тип кидаємо винятку предка, то ті місця які «чекають» предка, а отримують примірник «розширеного» нащадка могли б неконтрольовано викидати перевіряються винятку
 
 
 

8. Передача властивості у спадок

Нагадаємо ієрархію винятків з розставленими прапорами властивості checked / unchecked
 
Object
                      |
               Throwable(CHECKED)
               /            \
     Error(UNCHECKED)    Exception(CHECKED)
                            |
                  RuntimeException(UNCHECKED)

Логіка розташування властивості НЕ пов'язані зі спадкуванням. Цю логіку ми розглянемо пізніше (в наступних статтях).
Однак властивість checked / unchecked користувальницьких класів винятків будується ВИКЛЮЧНО НА ОСНОВІ НАСЛІДУВАННЯ.
Правило вкрай просте:
1. Якщо виключення зі списку Throwable, Error, Exception, RuntimeException — те твоє властивість треба просто запам'ятати.
2. Якщо ти не зі списку, то твоє властивість одно властивості предка. Порушити спадкування тут не можна.
 
Якщо ми породимо нащадків A, B, C, D, E, F, G, H, I, J, K, L які таким чином успадковуються від «кореневища» (Throwable, Error, Exception, RuntimeException), то значення їх властивості checked / unchecked можна побачити на схемі
 
Object
                      |
               Throwable(CHECKED)
               /    |       \
 Error(UNCHECKED)   |   Exception(CHECKED)
    |       |       |      |            |
    A(UNC)  D(UNC)  |      F©        RuntimeException(UNCHECKED)
  /   \             |     /   \             |       |
B(UNC) C(UNC)       |   G©  H©          I(UNC)  J(UNC)  
                   E©                   /   \ 
                                       K(UNC) L(UNC)

 
 

Контакти

 
Я займаюся онлайн навчанням Java (курси програмування ) і публікую частина навчальних матеріалів у рамках переробки курсу Java Core . Відеозаписи лекцій в аудиторії Ви можете побачити на youtube-каналі , можливо відео каналу краще систематизоване в цій статті .
Мій метод навчання полягає в тому, що я
 
     
  1. показую різні варіанти застосування
  2.  
  3. строю усложняющуюся послідовність прикладів по кожному варіанту
  4.  
  5. пояснюю логіку рухало авторами (в міру можливості)
  6.  
  7. даю велику кількість тестів (50-100) всебічно перевіряє розуміння і демонструють різні комбінації
  8.  
  9. даю лабораторні для самостійної роботи
  10.  
Дана статті слід пунктам # 1 (різні варіанти) і # 2 (послідовність прикладів по кожному варіанту).
 
skype: GolovachCourses
email: GolovachCourses@gmail.com
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.