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

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

throws — це ключове слово у сигнатурі методу, яке документує контракт: воно повідомляє викликаючому коду, які checked винятки метод може викинути і які викликаючий код зобов'яза...

Мовні версії: English Russian Ukrainian

Junior Level

Визначення

throws — це ключове слово у сигнатурі методу, яке документує контракт: воно повідомляє викликаючому коду, які checked винятки метод може викинути і які викликаючий код зобов’язаний бути готовий обробити.

public void readFile() throws IOException {
    Files.readAllLines(Paths.get("file.txt"));
}

Що це означає

Метод каже викликаючому коду: «Я можу не впоратися, і ти зобов’язаний знати про це».

Викликаючий код повинен:

  1. Огорнути виклик у try-catch
  2. Або теж додати throws у свою сигнатуру
// Варіант 1: try-catch
public void safeRead() {
    try {
        readFile();
    } catch (IOException e) {
        log.error("Failed", e);
    }
}

// Варіант 2: пробросити далі
public void process() throws IOException {
    readFile();
}

Кілька винятків

public void process() throws IOException, SQLException {
    // Може викинути обидва
}

Чому throws потрібен для checked, але не для unchecked

Checked винятки (наслідують Exception, але не RuntimeException) — компілятор вимагає їх обробки. throws — це спосіб методу делегувати відповідальність викликаючому: «Я не знаю, як обробити цю помилку, нехай викликаючий вирішує».

Unchecked винятки (наслідують RuntimeException) — компілятор не вимагає їх обробки. Вони представляють помилки програмування (NullPointerException, IllegalArgumentException), які не можна осмислено обробити — їх треба виправляти, а не ловити. Тому throws для них не потрібен.

// Checked: компілятор змушує оголосити або спіймати
public void readFile() throws IOException { ... }

// Unchecked: компілятор не вимагає throws
public void process(String s) {
    if (s == null) throw new NullPointerException();
}

Unchecked винятки та throws

Для RuntimeException throws технічно не потрібен, хоча мова і дозволяє його написати:

// Не потрібно писати throws NullPointerException
public void process(String s) {
    if (s == null) throw new NullPointerException();
}

Хоча void method() throws RuntimeException скомпілюється, це вважається поганим стилем — unchecked винятки не повинні засміювати сигнатуру.

Коли НЕ використовувати throws

  1. У публічному API бібліотекthrows Exception змушує клієнта ловити «все підряд»
  2. В інтерфейсах з численними checked винятками — ознака порушення SRP, розбийте інтерфейс
  3. У Spring REST контролерах — використовуйте @ControllerAdvice замість throws у кожному методі
  4. У лямбда-виразах — функціональні інтерфейси не підтримують checked винятки
  5. Для помилок програмуванняIllegalArgumentException, NullPointerException не оголошують через throws

Middle Level

Байт-код: атрибут Exceptions

У .class файлі інформація про throws зберігається в атрибуті методу Exceptions. JVM не використовує його для контролю в рантаймі — тільки для верифікатора компілятора.

При виклику через Reflection JVM не змусить ловити винятки:

Method m = MyClass.class.getMethod("readFile");
m.invoke(obj); // IOException не зобов'язана бути спійманою

Binary Compatibility

Додавання checked винятку ламає бінарну сумісність:

// Було:
public void process() { }

// Стало — усі клієнти перестануть компілюватися
public void process() throws IOException { }

Видалення винятку — бінарно сумісно (старий try-catch продовжить працювати).

Senior Tip: якщо потрібно додати перевірку в публічний API, використовуйте RuntimeException або створіть перевантажений метод.

Перевизначення та LSP

Принцип підстановки Лісков:

interface Service {
    void process() throws IOException;
}

class Impl implements Service {
    @Override
    // МОЖНА: не викидати взагалі (звуження контракту)
    public void process() { }

    @Override
    // МОЖНА: викидати підтип
    public void process() throws FileNotFoundException { }

    // НЕ МОЖНА: новий checked виняток — помилка компіляції
    // public void process() throws IOException, SQLException { }
}

Unchecked та throws

Технічно можна написати void method() throws RuntimeException, але це поганий тон. Unchecked винятки не повинні засміювати сигнатуру. Для документації використовуйте @throws у JavaDoc.


Senior Level

Throws як документація

Розглядайте throws як частину контракту. throws InsufficientFundsException каже про бізнес-логіку більше, ніж будь-який коментар.

Generic Throws

Java дозволяє використовувати дженерики:

public <E extends Exception> void doWork(ThrowingRunnable<E> task) throws E {
    task.run();
}

Активно використовується у функціональних інтерфейсах та обгортках.

Interface Segregation

Якщо інтерфейс викидає занадто багато різних throws — ознака порушення SRP. Метод робить занадто багато речей.

// Погано — занадто багато відповідальностей
public interface Service {
    void process() throws IOException, SQLException, AuthException, ValidationException;
}

// Добре — розбити на інтерфейси
public interface DataService {
    void save() throws DataAccessException;
}
public interface AuthService {
    void authenticate() throws AuthException;
}

// Ще краще — у Spring використовувати @ControllerAdvice
@RestController
public class OrderController {
    // throws не потрібен — @ControllerAdvice перехопить
    @PostMapping("/orders")
    public Order createOrder(@RequestBody OrderRequest req) {
        return orderService.create(req);
    }
}

ReflectiveOperationException

При Reflection усі checked винятки викликаного методу огортаються в InvocationTargetException.

throws Exception — небажаний патерн

У більшості випадків не варто писати throws Exception у публічному API. Це змушує клієнта ловити «все підряд», позбавляючи можливості адекватно реагувати на конкретні збої.

Діагностика

  • javap -v MyClass.class — побачите атрибут Exceptions у байт-коді
  • Binary compatibility check — інструменти на кшталт JAPICC перевіряють сумісність версій
  • Static Analysis — Sonar блокує throws Exception у публічних API

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

Обов’язково знати:

  • throws документує контракт методу — які checked винятки викликаючий зобов’язаний обробити
  • Checked винятки (не RuntimeException) компілятор вимагає оголосити або спіймати
  • Unchecked винятки (RuntimeException) не потребують throws — це помилки програмування
  • При перевизначенні можна звузити throws (прибрати або замінити підтипом), але не розширити
  • Додавання checked винятку ламає бінарну сумісність — клієнти перестануть компілюватися
  • У Spring REST використовуйте @ControllerAdvice замість throws у кожному методі
  • throws Exception у публічному API — антипатерн, змушує клієнта ловити «все підряд»

Часті уточнюючі запитання:

  • Обов’язково чи ловити checked винятки? — Так, або try-catch, або свій throws
  • Чи можна додати throws RuntimeException? — Технічно так, але поганий стиль
  • Що буде при перевизначенні методу? — Можна прибрати throws або звузити, але не розширити (LSP)
  • Як throws зберігається у байт-коді? — Атрибут Exceptions у .class, JVM не перевіряє в рантаймі

Червоні прапорці (НЕ говорити):

  • “Пишу throws Exception всюди для простоти” — антипатерн, клієнт не може адекватно обробити
  • “Не оголошую checked винятки — компілятор не помітить” — не скомпілюється
  • throws впливає на продуктивність у рантаймі” — ні, це лише компіляторна перевірка
  • “Можна додати новий checked виняток без наслідків” — ламає бінарну сумісність

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

  • [[22. Чи можна викинути checked виняток з методу без throws]] — обхід через sneaky throws
  • [[2. Що таке checked exception і коли його використовувати]] — природа checked винятків
  • [[3. Що таке unchecked exception (Runtime Exception)]] — чому не потребують throws
  • [[28. Чи можна перехопити і викинути виняток заново]] — rethrow зі збереженням throws
  • [[13. Чи можна створювати кастомні винятки]] — свої винятки для throws