1. Wprowadzenie do bloku finally
Gdy pracujesz z zasobami — plikami, połączeniami sieciowymi, bazami danych — ważne jest, aby mieć pewność, że będą zamknięte lub zwolnione zawsze, nawet jeśli w trakcie pracy pojawi się błąd. W Java służy do tego specjalny blok — finally.
Jak działa finally?
Blok finally to część konstrukcji try-catch-finally. Kod wewnątrz finally wykonuje się zawsze (jeśli został napisany) — niezależnie od tego, czy wystąpił wyjątek, czy nie. Nawet jeśli w try był return lub rzucono wyjątek — finally i tak się wykona (chyba że wyłączono komputer albo program zakończono wymuszeniem przez System.exit(0)).
Składnia:
try {
// Kod, w którym może wystąpić wyjątek
} catch (ExceptionType e) {
// Obsługa błędu
} finally {
// Ten kod wykona się zawsze!
}
Przykład
try {
System.out.println("Początek pracy");
int result = 10 / 0; // tutaj wystąpi błąd
System.out.println("Wynik: " + result);
} catch (ArithmeticException e) {
System.out.println("Błąd: dzielenie przez zero");
} finally {
System.out.println("Ten kod wykona się w każdym przypadku");
}
Wynik działania:
Początek pracy
Błąd: dzielenie przez zero
Ten kod wykona się w każdym przypadku
Co się dzieje?
- W try próbujemy podzielić dwie liczby i otrzymujemy błąd.
- Jeśli podczas dzielenia wystąpi błąd — catch go obsłuży.
- Ale! W każdym przypadku finally wykona się i wypisze komunikat na konsolę.
2. finally bez catch
Istnieją 3 warianty konstrukcji:
- Pełny: try-catch-finally
- Bez finally: try-catch
- Bez catch: try-finally
Trzeci wariant stosuje się, gdy przechwytywanie i obsługa błędu nastąpi w metodzie o poziom wyżej. Jednak blok finally jest potrzebny, aby zagwarantować wykonanie określonego kodu:
- Zamykanie plików, połączeń sieciowych, baz danych.
- Zwalnianie wszelkich zasobów (np. blokad).
- Logowanie: zapis informacji o zakończeniu operacji.
Przykład:
try {
System.out.println("Dzielimy liczby");
int result = 10 / 0; // błąd!
System.out.println("Wynik: " + result);
} finally {
System.out.println("Blok finally został wykonany");
}
Wynik:
Dzielimy liczby
Blok finally został wykonany
Exception in thread "main" java.lang.ArithmeticException: / by zero
Kiedy finally NIE jest wykonywany?
Wykonywany jest prawie zawsze. Wyjątki — gdy:
- Wymuszono zakończenie programu za pomocą: System.exit(0).
- Wymuszenie „zabito” wątek, w którym wykonywany jest finally.
- Wyłączono komputer.
3. Operator throw: jak samodzielnie wygenerować wyjątek
Czasem Java sama „rzuca” wyjątki (np. dzielenie przez zero, wyjście poza granice tablicy). Ale zdarzają się sytuacje, gdy chcesz wyraźnie zakomunikować: „To błąd! Nie mogę kontynuować wykonania!”. Do tego w Java służy operator throw.
Analogia: Jeśli w sklepie widzisz przeterminowany towar — „rzucasz” skargę. Tak samo w kodzie: jeśli coś jest nie tak — rzucasz wyjątek.
Składnia throw
throw new ExceptionType("Komunikat o błędzie");
ExceptionType — dowolna klasa dziedzicząca po Throwable (zwykle Exception lub RuntimeException). W nawiasach — komunikat, który pomoże zrozumieć, co poszło nie tak.
Przykład: weryfikacja argumentów metody
public static int safeDivide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("Dzielnik nie może być równy zeru");
}
return a / b;
}
Użycie:
public static void main(String[] args) {
try {
int result = safeDivide(10, 0);
System.out.println("Wynik: " + result);
} catch (IllegalArgumentException e) {
System.out.println("Błąd: " + e.getMessage());
}
}
Wynik:
Błąd: Dzielnik nie może być równy zeru
Kiedy używać throw?
- Weryfikacja argumentów metody (np. jeśli przyszedł null lub niepoprawne dane).
- Weryfikacja stanu obiektu (np. próba wypłaty środków z konta, na którym jest 0 euro).
- Wewnątrz catch — jeśli chcesz „przerzucić” wyjątek dalej (np. dodać dodatkowe informacje).
4. Łączenie try-catch-finally i throw
Czasem te konstrukcje działają razem. Na przykład łapiesz jeden błąd, ale potem decydujesz się rzucić własny, bardziej informacyjny.
public static int parseAndDivide(String text, int divisor) {
try {
int number = Integer.parseInt(text);
if (divisor == 0) {
throw new IllegalArgumentException("Dzielnik nie może być równy zeru");
}
return number / divisor;
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Ciąg '" + text + "' nie jest liczbą");
} finally {
System.out.println("Próba przetworzenia ciągu: " + text);
}
}
Użycie:
try {
int result = parseAndDivide("42a", 2);
System.out.println("Wynik: " + result);
} catch (IllegalArgumentException e) {
System.out.println("Błąd: " + e.getMessage());
}
Wynik:
Próba przetworzenia ciągu: 42a
Błąd: Ciąg '42a' nie jest liczbą
Ważna uwaga: return i finally
Nawet jeśli w bloku try jest return, finally i tak się wykona!
public static int getValue() {
try {
return 10;
} finally {
System.out.println("finally i tak się wykona!");
}
}
Wywołanie getValue() wypisze:
finally i tak się wykona!
5. Typowe błędy przy użyciu finally i throw
Błąd nr 1: zapomniano zamknąć zasób bez finally.
Bardzo częsty problem: otwarto plik, nie zamknięto — doszło do wycieku zasobów. Zawsze używaj finally (lub try-with-resources, o którym powiemy później).
Błąd nr 2: rzucono wyjątek, ale go nie obsłużono.
Jeśli rzucasz wyjątek za pomocą throw, ale nigdzie go nie łapiesz (brak try-catch), program zakończy się awaryjnie. Zawsze myśl, kto będzie łapał twój wyjątek.
Błąd nr 3: return w finally.
Jeśli przez pomyłkę napiszesz return wewnątrz finally, „nadpisze” on wszystkie wcześniejsze return lub throw. To może prowadzić do bardzo trudnych do wychwycenia błędów. Tak robić jest kategorycznie odradzane!
public int tricky() {
try {
return 1;
} finally {
return 2; // NIEBEZPIECZNE: zwróci 2, a nie 1!
}
}
Wynik: zostanie zwrócone 2, chociaż w try było 1.
Błąd nr 4: utrata informacji o wyjątku.
Jeśli łapiesz jeden wyjątek, a potem rzucasz nowy, nie zachowując informacji o starym (e), tracisz stos wywołań, co utrudnia debugowanie. Lepiej pisać tak:
catch (NumberFormatException e) {
throw new IllegalArgumentException("Błąd konwersji", e);
}
GO TO FULL VERSION