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

Различни грешки и finally

Проблем

Сметката за нула души

Програмата дели сметката в ресторанта по равно. Вече знаеш за грешния вход и го хващаш:

Main.java
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
try {
int bill = Integer.parseInt(in.nextLine());
int people = Integer.parseInt(in.nextLine());
System.out.println("На човек: " + bill / people + " €");
} catch (NumberFormatException e) {
System.out.println("Въведи цели числа.");
}
}
}

Сметката е 90 €, а някой въвежда 0 за броя на хората. Програмата пак спира:

Грешка
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Main.main(Main.java:9)

catch е там, но не помага. Хваща само NumberFormatException, а това е друг вид изключение.

Идеята

Няколко catch

След един try може да има няколко catch, по един за всеки вид грешка:

Scanner in = new Scanner(System.in);
try {
int bill = Integer.parseInt(in.nextLine());
int people = Integer.parseInt(in.nextLine());
System.out.println("На човек: " + bill / people + " €");
} catch (NumberFormatException e) {
System.out.println("Въведи цели числа.");
} catch (ArithmeticException e) {
System.out.println("Хората не може да са 0.");
}
Вход Изход
90 и 3 На човек: 30 €
90 и 0 Хората не може да са 0.
90 и трима Въведи цели числа.

Блоковете се проверяват отгоре надолу. Изпълнява се първият, чийто вид отговаря, и само той. Останалите се прескачат.

Scanner in = new Scanner(System.in);
try {
int bill = Integer.parseInt(in.nextLine());
int people = Integer.parseInt(in.nextLine());
System.out.println("На човек: " + bill / people + " €");
} catch (NumberFormatException e) {
System.out.println("Въведи цели числа.");
} catch (ArithmeticException e) {
System.out.println("Хората не може да са 0.");
}
Стъпка по стъпка, с думи
  1. Първо пускане: сметка 90 €, хората са 0.
  2. 90 / 0 е деление на нула: хвърля се ArithmeticException.
  3. Първият catch е за NumberFormatException. Не пасва, прескача се.
  4. Вторият пасва. Изпълнява се той и само той.
  5. Второ пускане: вместо брой хора е написано „трима“.
  6. parseInt не може да направи число: хвърля се NumberFormatException.
  7. Пасва още първият catch. До втория не се стига.
  8. Блоковете се проверяват отгоре надолу. Изпълнява се първият, който пасва.
По-точно, за любопитните
  • Когато два вида се обработват еднакво, можеш да ги хванеш в един блок с |: catch (NumberFormatException | ArithmeticException e).

Идеята

Семейството на изключенията

Изключенията са класове, свързани с наследяване. Ето най-честите:

Йерархията
Throwable
├── Error сериозен проблем на JVM, не се хваща
│ ├── OutOfMemoryError
│ └── StackOverflowError
└── Exception
├── IOException файлове, мрежа
├── InterruptedException
└── RuntimeException
├── ArithmeticException
├── IllegalArgumentException
│ └── NumberFormatException
├── IndexOutOfBoundsException
│ └── ArrayIndexOutOfBoundsException
└── NullPointerException

catch хваща посочения вид и всичките му наследници. Това е полиморфизмът от модула за наследяването: променлива от родителски тип може да сочи обект на наследника. Затова catch (Exception e) хваща всичко:

static void split(String billText, String peopleText) {
try {
int bill = Integer.parseInt(billText);
int people = Integer.parseInt(peopleText);
System.out.println("На човек: " + bill / people + " €");
} catch (Exception e) {
System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
}
}
split("90", "3");
split("90", "0");
split("90", "трима");
Изход
На човек: 30 €
ArithmeticException: / by zero
NumberFormatException: For input string: "трима"

e.getClass().getSimpleName() дава името на истинския клас на обекта. Променливата е от тип Exception, но обектът е от наследник.

По-точно, за любопитните
  • Error и наследниците му са проблеми на самата JVM, например свършена памет или препълнен стек. Хвърля ги самата JVM. Програмата обикновено не може да направи нищо разумно след тях, затова не се хващат.
  • catch (Throwable t) би хванал и тях. Не го прави.
  • Хващай толкова конкретно, колкото можеш. catch (Exception e) скрива и грешки, които не си очаквал, например бъг в собствения ти код.

Пробвай

Кой catch?

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

int[] seats = { 4, 2, 6 };
try {
System.out.println("Маса 4: " + seats[3] + " места");
} catch (ArithmeticException e) {
System.out.println("Деление на нула");
} catch (RuntimeException e) {
System.out.println("Грешка при изпълнение");
} catch (Exception e) {
System.out.println("Обща грешка");
}
Отговор

Пробвай

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

Тази програма не се компилира. На кой ред е грешката?

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

Идеята

Проверени изключения и throws

Някои изключения компилаторът задължава да ги обработиш. Ето метод, който печата бележка и чака принтера с Thread.sleep (пауза в милисекунди):

static void printReceipt() {
System.out.println("Печатам бележката...");
Thread.sleep(300);
System.out.println("Готово.");
}

Програмата не се компилира: „unreported exception InterruptedException; must be caught or declared to be thrown“. Паузата може да бъде прекъсната отвън, а компилаторът иска да кажеш какво да стане тогава. Имаш два избора: да хванеш изключението в метода или да го обявиш с throws и да оставиш викащия да реши:

static void printReceipt() throws InterruptedException {
System.out.println("Печатам бележката...");
Thread.sleep(300);
System.out.println("Готово.");
}
try {
printReceipt();
} catch (InterruptedException e) {
System.out.println("Прекъснато.");
}
Изход
Печатам бележката...
Готово.
  • Проверени (checked) са наследниците на Exception, които не са RuntimeException. Обикновено идват отвън, от неща извън програмата: липсващ файл, прекъсната мрежа, прекъсната пауза.
  • Непроверени (unchecked) са RuntimeException и наследниците му. Обикновено са грешка в кода или във входа: индекс, деление на нула, грешен формат. Компилаторът не задължава да ги хващаш.

Най-често ще срещнеш проверено изключение при файловете: IOException.

Идеята

finally: винаги

Принтерът на касата трябва да се освободи след всяка бележка, иначе следващата няма да се отпечата. Каквото и да стане, успех или грешка:

static void printReceipt(String qtyText) {
System.out.println("Принтерът е зает.");
try {
int qty = Integer.parseInt(qtyText);
System.out.println("Кафе: " + qty + " x 2 € = " + qty * 2 + " €");
} catch (NumberFormatException e) {
System.out.println("Грешно количество: " + qtyText);
} finally {
System.out.println("Принтерът е свободен.");
}
}
printReceipt("3");
printReceipt("три");
Изход
Принтерът е зает.
Кафе: 3 x 2 € = 6 €
Принтерът е свободен.
Принтерът е зает.
Грешно количество: три
Принтерът е свободен.

finally се изпълнява винаги: когато всичко мине, когато изключението е хванато и дори когато нито един от тези catch не го хваща. В последния случай finally се изпълнява, а изключението продължава нагоре към следващия try, който може да го хване.

Защо не просто ред след try/catch? Защото той се пропуска, ако изключението е от вид, който catch не хваща, или ако в try има return. finally не се пропуска.

По-точно, за любопитните
  • Може и try с finally, без catch: изключението не се хваща, но почистването се изпълнява.
  • За ресурси, които трябва да се затварят, като файлове и връзки с база данни, има по-кратък запис: try (...) с ресурс в скобите (try-with-resources). Ще го видим, когато стигнем до файловете. Той сам затваря ресурса, както би го затворил finally.
  • finally не се изпълнява само ако програмата бъде спряна изцяло, например с System.exit(0).

Пробвай

finally и return

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

static String reserve(int guests) {
try {
if (guests > 6) {
return "Няма маса за " + guests;
}
return "Маса за " + guests;
} finally {
System.out.println("Записах опита.");
}
}
System.out.println(reserve(8));
Отговор

Упражнение

Програми с няколко catch

Реши задачите в редактора си. Не печатай въпроси.

1. Цена на човек. Цените са в масив { 120, 75, 300 }. Прочети номер на продукт (от 1) и броя на хората и отпечатай цената на човек с целочислено деление. Хвани три неща: несъществуващ продукт, нула души и всичко останало (тогава отпечатай съобщението на изключението).

Вход Изход
2 и 3 На човек: 25 €
5 и 2 Няма такъв продукт.
1 и 0 Броят на хората не може да е 0.
1 и двама Грешка: For input string: "двама"

2. Калкулатор. Прочети цяло число, оператор (+, -, * или /) и второ цяло число, всяко на свой ред. Отпечатай сметката. При текст вместо число кажи „Въведи цели числа.“, при деление на нула „Деление на нула.“, а при друг оператор „Непознат оператор: “ и оператора.

Вход
84
/
4
Изход
84 / 4 = 21
Вход
84
/
0
Изход
Деление на нула.

Още задачи

  • Терминалът. Метод pay(String amountText) печата „Терминалът е свързан.“, после „Платени X €“ или съобщение за грешен вход, а във finally „Терминалът е изключен.“ Извикай го с „25“ и с „двайсет“.
  • Средна оценка без сривове. Прочети 5 реда с оценки. Пропусни тези, които не са цели числа (изключение) или не са от 2 до 6 (if). Отпечатай средното с целочислено деление. Ако нито една оценка не е вярна, деленето на нула хвърля: хвани го и отпечатай „Няма верни оценки.“
  • Предвиди и провери. Напиши try с catch и finally, в който всеки блок печата по ред. Предвиди изхода при успех, при хванато и при нехванато изключение и после го пусни.
  • Пауза. Напиши метод pause(int ms), който вика Thread.sleep(ms). Направи го веднъж с throws InterruptedException, веднъж с try/catch вътре. Какво трябва да направи main във всеки от двата случая?

Тест

Провери се

1. Изключението отговаря на два от трите catch блока. Кой се изпълнява?
2. Хваща ли catch (RuntimeException e) изключение NumberFormatException?
3. Защо catch (Exception e) трябва да е последен?
4. Кога се изпълнява finally?
5. Кое изключение е проверено (компилаторът задължава да го хванеш или да го обявиш с throws)?

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