⚡ Розділ 7 · Питання #21

Що робить ключове слово throws

Ключове слово throws вказується в сигнатурі методу або конструктора для декларації перевірюваних (checked) винятків, які цей метод потенційно може викинути під час свого виконання.


🟢 Junior Level

Коротка відповідь для співбесіди (30 секунд)

Ключове слово throws вказується в сигнатурі методу або конструктора для декларації перевірюваних (checked) винятків, які цей метод потенційно може викинути під час свого виконання.

Це частина відкритого API-контракту методу. Наявність throws повідомляє компілятору javac, що метод делегує обробку помилки вище по стеку, і змушує будь-який викликаючий код обрати один із двох варіантів:

  1. Перехопити виняток на місці за допомогою блоку try-catch.
  2. Продекларувати цей самий виняток (або його суперклас) у своїй власній секції throws.

Різниця між throw та throws

Ключове слово throw throws
Де використовується Усередині тіла методу або блоку коду У заголовку (сигнатурі) методу або конструктора
Роль Оператор дії: негайно створює та ініціює викидання об’єкта винятку Елемент декларації: попереджає про типи винятків, які можуть вилетіти з методу
Синтаксис throw new IllegalArgumentException(); public void run() throws IOException;
Аргумент Конкретний екземпляр об’єкта (Throwable) Список типів класів через кому

Базовий приклад на Java 21

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public class FileReaderService {

    // Декларуємо, що метод може викинути checked IOException
    public List<String> readLines(String filePath) throws IOException {
        return Files.readAllLines(Path.of(filePath));
    }

    // Варіант 1: Викликаючий код перехоплює помилку локально
    public void processSafely(String path) {
        try {
            List<String> lines = readLines(path);
            System.out.println("Lines count: " + lines.size());
        } catch (IOException e) {
            System.err.println("Failed to read file: " + e.getMessage());
        }
    }

    // Варіант 2: Викликаючий код делегує відповідальність ще вище
    public void processAndPropagate(String path) throws IOException {
        List<String> lines = readLines(path);
        // подальша обробка
    }
}

🟡 Middle Level

Правила перевизначення методів та LSP (Liskov Substitution Principle)

Під час перевизначення методів у підкласах або реалізації інтерфейсів правила поліморфізму в Java суворо обмежують секцію throws відповідно до принципу підстановки Лісков:

public interface BaseRepository {
    void save() throws IOException;
}

public class SqlRepository implements BaseRepository {
    // 1. МОЖНА: звузити виняток до його підкласу (коваріантність винятків)
    @Override
    public void save() throws java.io.FileNotFoundException { }
}

public class MemoryRepository implements BaseRepository {
    // 2. МОЖНА: повністю прибрати throws, якщо реалізація не викидає перевірюваних помилок
    @Override
    public void save() { }
}

public class BadRepository implements BaseRepository {
    // 3. НЕ МОЖНА: додати новий checked виняток або ширший тип!
    // ПОМИЛКА КОМПІЛЯЦІЇ: overridden method does not throw java.sql.SQLException
    // @Override
    // public void save() throws SQLException { }

    // 4. НЕ МОЖНА: розширити до супертипу!
    // ПОМИЛКА КОМПІЛЯЦІЇ: Exception is not compatible with throws clause in BaseRepository
    // @Override
    // public void save() throws Exception { }
}

Чому так влаштовано: Клієнт, що викликає BaseRepository.save(), готовий зловити лише IOException. Якби підклас викинув непродекларований SQLException, блок catch (IOException e) пропустив би помилку, порушивши типобезпеку.


Unchecked винятки та throws

Синтаксис Java дозволяє оголошувати в throws неперевірювані (unchecked) винятки (RuntimeException та Error):

// Технічно компілюється, але вважається стилістичною помилкою
public void calculate(int val) throws IllegalArgumentException, NullPointerException {
    // ...
}

Практичне правило: Не вказуйте unchecked-винятки в секції throws. Вони не перевіряються компілятором, захаращують сигнатуру методу й вводять розробників в оману. Для документування можливих рантайм-помилок використовуйте тег Javadoc @throws IllegalArgumentException.


Байткод і JVM: Атрибут Exceptions

На рівні JVM віртуальна машина не перевіряє відповідність throws у runtime. Інформація про throws компілюється у .class-файл у спеціальний атрибут методу Exceptions:

// Результат декомпіляції javap -v FileReaderService.class:
public java.util.List<java.lang.String> readLines(java.lang.String) throws java.io.IOException;
  descriptor: (Ljava/lang/String;)Ljava/util/List;
  flags: (0x0001) ACC_PUBLIC
  Exceptions:
    throws java.io.IOException

Цей атрибут використовується виключно компілятором javac для перевірки коректності клієнтського коду. У разі виклику методу через Java Reflection (Method.invoke()) перевірювані винятки безперешкодно вилітають назовні (будучи загорнутими в InvocationTargetException), доводячи, що рантайм HotSpot не валідує виклики за throws.


🔴 Senior Level

Тонкість конструкторів: винятки суперкласу

Поведінка throws у конструкторах фундаментально відрізняється від методів:

public class Parent {
    public Parent() throws IOException {
        // Ініціалізація ресурсу
    }
}

public class Child extends Parent {
    // Конструктор підкласу ЗОБОВ'ЯЗАНИЙ оголосити throws IOException (або ширший тип)!
    // Він НЕ МОЖЕ перехопити виняток конструктора суперкласу всередині свого тіла!
    public Child() throws IOException, Exception {
        super(); // Перший рядок конструктора зобов'язаний бути super()!
    }
}

На відміну від методів, конструктор підкласу не може прибрати throws батька, оскільки інструкція super(...) зобов’язана бути першим рядком, і огорнути її в try-catch синтаксично неможливо.


Узагальнені типи у винятках (Generic Throws)

Java підтримує параметризацію секції throws типом винятку. Це основа для створення універсальних функціональних адаптерів:

@FunctionalInterface
public interface ThrowingConsumer<T, E extends Throwable> {
    void accept(T t) throws E;
}

public class ExceptionAdapter {
    public static <T, E extends Throwable> void execute(T value, ThrowingConsumer<T, E> consumer) throws E {
        consumer.accept(value);
    }

    public static void demo() {
        try {
            // Тип E автоматично виводиться компілятором як IOException
            execute("file.txt", path -> {
                throw new java.io.IOException("Disk read error");
            });
        } catch (java.io.IOException e) {
            System.out.println("Caught precisely inferred IOException!");
        }
    }
}

Зміни API, що ламають сумісність (Binary & Source Compatibility)

У бібліотечній розробці зміна сигнатури throws є критичною подією:

  1. Додавання нового checked-винятку: Ламає вихідну сумісність (Source Compatibility). Будь-який клієнтський проєкт перестане компілюватися під час оновлення версії бібліотеки.
  2. Видалення checked-винятку: Вихідно та бінарно сумісно (старий клієнтський блок try-catch продовжить працювати, хоча компілятор може видати попередження про мертве перехоплення).
  3. Заміна конкретного винятку на throws Exception: Антипатерн “Throws Umbrella”, що знищує семантику API.

🎯 Шпаргалка для інтерв’ю

Питання для швидкої перевірки

  1. Чи можна в перевизначеному методі підкласу прибрати checked виняток, оголошений у базовому класі? Відповідь: Так, можна. Підклас має право звузити контракт винятків або зовсім не викидати їх. Це повністю відповідає принципу LSP (клієнт, готовий до помилки, просто ніколи з нею не зіткнеться).
  2. Чи може конструктор підкласу приховати checked виняток, що викидається конструктором суперкласу? Відповідь: Ні, не може. Першим рядком будь-якого конструктора зобов’язаний бути виклик super(...). Цей виклик неможливо огорнути в блок try-catch, тому конструктор підкласу зобов’язаний оголосити в throws усі checked-винятки конструктора батька (або їх супертипи).
  3. Що станеться, якщо викликати метод із throws IOException через Reflection API (Method.invoke)? Відповідь: Компілятор не вимагатиме обов’язкового перехоплення IOException. Якщо метод викине IOException, рефлексія загорне його в InvocationTargetException, демонструючи, що перевірка throws — це виключно статичний аналіз javac.
  4. Чому throws Exception у публічному інтерфейсі вважається поганою практикою? Відповідь: Це розмиває контракт API. Викликаючий код втрачає інформацію про реальні можливі відмови та змушений писати неспецифічне перехоплення catch (Exception e), яке ненавмисно маскує помилки часу виконання (NullPointerException, IllegalArgumentException).

Типові помилки та Red Flags

  • ❌ Плутанина між термінами throw та throws: Пояснення throws як «команди, яка викидає помилку в коді».
  • ❌ Спроба оголосити в методі підкласу новий checked виняток: Нерозуміння обмежень LSP та перевизначення методів.
  • ❌ Вказування неперевірюваних винятків (throws RuntimeException): Зайвий візуальний шум, що не дає жодних гарантій компілятора.
  • ❌ Використання throws Exception як “універсального рішення”: Позбавляє архітектуру типізованої обробки помилок.

Пов’язані теми