Към съдържанието
wannacode

Хвърляне и свои изключения

Проблем

Банкоматът казва „вземи парите“

Сметката пази баланса си. Ако сумата за теглене е отрицателна или не стига, методът просто не прави нищо:

public void withdraw(int amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
}
}

Банкоматът го ползва така:

Account account = new Account(100);
account.withdraw(500);
System.out.println("Вземи парите си.");
System.out.println("Баланс: " + account.getBalance() + " €");
Изход
Вземи парите си.
Баланс: 100 €

Клиентът иска 500 €, а има 100 €. Методът тихо отказва, а банкоматът не разбира и казва „Вземи парите си.“ Грешката се губи. Методът може да върне true или false, но викащият лесно забравя да провери, а и false не казва защо.

Идеята

throw: отказвам и казвам защо

Вместо да мълчи, методът хвърля изключение:

public void withdraw(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Сумата трябва да е положителна");
}
if (amount > balance) {
throw new IllegalArgumentException("Не стигат парите");
}
balance -= amount;
}

throw прекъсва метода като return. Редът balance -= amount не се стига. Но вместо стойност методът изпраща изключение и викащият не може да не го забележи:

Account account = new Account(100);
try {
account.withdraw(500);
System.out.println("Вземи парите си.");
} catch (IllegalArgumentException e) {
System.out.println("Отказано: " + e.getMessage());
}
System.out.println("Баланс: " + account.getBalance() + " €");
Изход
Отказано: Не стигат парите
Баланс: 100 €
  • new създава обекта на изключението, както всеки обект. Съобщението отива в конструктора му.
  • IllegalArgumentException е готовото изключение за „аргументът е грешен“. Ползва се точно в такива проверки.
По-точно, за любопитните
  • Друго често готово изключение е IllegalStateException: обектът не е в състояние за това действие, например теглене от закрита сметка.
  • throw работи с всеки обект от наследник на Throwable. Текст или число не може да се хвърли.

Виж го

Изключението пропада

public void withdraw(int amount) {
if (amount > balance) {
throw new IllegalArgumentException("Не стигат парите");
}
balance -= amount;
}
static void payRent(Account account) {
account.withdraw(800);
System.out.println("Наемът е платен.");
}
try {
payRent(new Account(500));
} catch (IllegalArgumentException e) {
System.out.println("Отказано: " + e.getMessage());
}
Стъпка по стъпка, с думи
  1. main вика payRent за сметка с 500 €.
  2. payRent вика withdraw(800): нова рамка отгоре.
  3. 800 > 500: методът хвърля изключение.
  4. Балансът не се променя: рамката се изхвърля, без да довърши редовете си.
  5. payRent няма catch: „Наемът е платен.“ не се печата, рамката се изхвърля.
  6. catch в main хваща изключението и програмата продължава оттук.

Спомни си стека от извиквания от урока за рекурсията. main вика payRent, а той вика withdraw. Рамките са една над друга.

Когато withdraw хвърли, изключението търси catch, като слиза надолу по стека. Рамка без подходящ catch се изхвърля веднага, без да довърши редовете си. Така е и с payRent. Чак в main има catch и програмата продължава оттам.

Ако и main нямаше catch, щеше да се изхвърли и той. Тогава програмата спира и печата stack trace: точно тези рамки, отвътре навън.

Пробвай

Какво стига до екрана?

Сметката има 50 €, а поръчката е 120 €.

Какво ще отпечата програмата?

static void checkout(Account account) {
System.out.println("Плащам поръчката");
account.withdraw(120);
System.out.println("Поръчката е платена");
}
Account account = new Account(50);
try {
checkout(account);
System.out.println("Пращам потвърждение");
} catch (IllegalArgumentException e) {
System.out.println("Отказано: " + e.getMessage());
}
System.out.println("Баланс: " + account.getBalance() + " €");
Отговор

Пробвай

Намери грешката

Наемът е 800 €, а в сметката има 500 €. Програмата казва „Наемът е платен.“ На кой ред е грешката?

Избери реда с грешката.

По-точно, за любопитните
  • Понякога методът хваща, прави нещо, например записва в дневник, и хвърля същото изключение нататък: throw e; в catch.
  • Може и да хвърлиш друго изключение, което по-добре казва какво е станало. Тогава подай старото като втори аргумент на конструктора: new IllegalStateException("Наемът не е платен", e). В stack trace-а ще видиш и двете.

Идеята

Свое изключение

„Не стигат парите“ е текст. Ако банкоматът иска да предложи „Внеси още 30 €“, трябва да знае колко не стигат. Да вади числото от текста е крехко. По-добре направи свой клас за изключение с поле:

class InsufficientFundsException extends RuntimeException {
private int shortfall;
public InsufficientFundsException(int shortfall) {
super("Не стигат " + shortfall + " €");
this.shortfall = shortfall;
}
public int getShortfall() {
return shortfall;
}
}

Своето изключение е обикновен клас, който наследява RuntimeException. Конструкторът подава съобщението на родителя с super(...). Методът хвърля него, когато парите не стигат:

public void withdraw(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Сумата трябва да е положителна");
}
if (amount > balance) {
throw new InsufficientFundsException(amount - balance);
}
balance -= amount;
}

А викащият хваща двата случая поотделно:

Account account = new Account(100);
try {
account.withdraw(130);
} catch (InsufficientFundsException e) {
System.out.println(e.getMessage());
System.out.println("Внеси още " + e.getShortfall() + " € или избери по-малка сума.");
} catch (IllegalArgumentException e) {
System.out.println("Отказано: " + e.getMessage());
}
Изход
Не стигат 30 €
Внеси още 30 € или избери по-малка сума.
По-точно, за любопитните
  • Имената на изключенията винаги завършват на Exception.
  • Своето изключение може да наследи и по-конкретен готов клас, например IllegalArgumentException. Тогава catch за родителя хваща и него. Тук то е отделно, затова редът на двата catch няма значение.

Идеята

Exception или RuntimeException?

Ако своето изключение наследи Exception, а не RuntimeException, то е проверено. Методът трябва да го обяви с throws, а всеки викащ трябва да го хване или също да го обяви:

class InsufficientFundsException extends Exception { ... }
public void withdraw(int amount) throws InsufficientFundsException {
...
}
  • RuntimeException, когато грешката е в аргументите или в правилата на програмата. Викащият може да я предотврати с проверка. Повечето свои изключения днес са такива.
  • Exception, когато викащият трябва да е готов за нея всеки път и компилаторът трябва да го напомня.

Готово изключение, когато смисълът съвпада: IllegalArgumentException за грешен аргумент, IllegalStateException за действие в неподходящ момент. Свое, когато викащият трябва да го различи от другите и да реагира по свой начин или когато изключението трябва да носи данни, като shortfall.

Пробвай

Попълни празнината

Изключение за невалидна възраст. Какво трябва да пише на празното място, за да стигне съобщението до родителя?

class InvalidAgeException extends RuntimeException {
    public InvalidAgeException(String message) {
        (message);
    }
}

Упражнение

Изключения в класовете

Реши задачите в редактора си.

1. Музикален магазин. Магазинът пуска инструменти на демо, но не всеки може да се пусне.

  • Абстрактен клас MusicalInstrument с име, марка и цена (частни полета с getter-и) и конструктор.
  • Интерфейс Playable с метод play().
  • Guitar и Piano наследяват инструмента и могат да свирят. Drum само наследява.
  • NotPlayableException extends RuntimeException.
  • MusicShop с метод demoInstrument(MusicalInstrument instrument): ако инструментът може да свири (instanceof Playable playable), пуска го, иначе хвърля NotPlayableException.
  • В main пусни на демо китара Fender Stratocaster (1299 €), барабани Pearl Export (849 €) и пиано Yamaha P-45 (499 €) от масив. Хвани изключението за всеки инструмент поотделно, за да продължи демото.
Изход
Fender Stratocaster, 1299.00 €
Китарата свири акорд.
Pearl Export, 849.00 €
Pearl Export не може да се пусне на демо.
Yamaha P-45, 499.00 €
Пианото свири гама.

2. Сметка с правила. Добави към Account метод deposit(int amount), който хвърля IllegalArgumentException при сума 0 или по-малко. Тегленето хвърля InsufficientFundsException от урока. Сметката започва със 100 €. Прочети брой операции и после самите операции, всяка на свой ред: „внеси 50“ или „изтегли 500“. След всяка отпечатай баланса или защо операцията е отказана.

Вход
4
внеси 50
изтегли 500
изтегли -5
изтегли 120
Изход
Баланс: 150 €
Не стигат 350 €. Баланс: 150 €
Сумата трябва да е положителна. Баланс: 150 €
Баланс: 30 €

3. Невалидна възраст. Person има име и възраст. setAge хвърля InvalidAgeException от картата преди тази, ако възрастта е под 0 или над 150. Конструкторът също минава през setAge, за да не може да се създаде човек с невалидна възраст. Създай Мария (19), Петър (−3) и Иван (200).

Изход
Мария, 19 г.
Петър: Невалидна възраст: -3
Иван: Невалидна възраст: 200

Още задачи

  • Резервация на маса. Клас Restaurant с брой свободни маси и метод за резервация. Ако масите са свършили, хвърли свое NoTablesException. Ако броят на гостите е 0 или по-малко, хвърли готовото изключение за грешен аргумент.
  • Продукт със склад. Клас Product с наличност и метод sell(int qty). Ако наличността не стига, хвърли OutOfStockException, което носи колко бройки има. Магазинът хваща изключението и предлага да продаде наличните.
  • Музикален магазин с причина. Добави към NotPlayableException поле с инструмента, който не може да свири, и отпечатай цената му в catch.

Тест

Провери се

1. Какво прави throw new IllegalArgumentException("...")?
2. main вика a, a вика b, b хвърля изключение. Само main има catch. Какво става с редовете в a след извикването на b?
3. Къде е най-добре да хванеш изключението?
4. Свое изключение наследява Exception, а не RuntimeException. Какво следва?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·