1. Wyjątki jako część API
Dlaczego wyjątki to część „kontraktu” metody?
Pisząc metodę, określasz nie tylko parametry i wartość zwracaną, ale też jakie wyjątki może ona zgłosić. To część „kontraktu” między twoją metodą a tymi, którzy będą jej używać. Jeśli metoda może zgłosić wyjątek, powinni o tym wiedzieć wszyscy, którzy ją wywołują – aby poprawnie obsłużyć błąd (albo przynajmniej nie dziwić się, gdy program nagle zakończy się z błędem).
Przykład:
public void readFile(String filename) throws IOException {
// ... odczyt pliku
}
Tutaj wyraźnie wskazano, że metoda może zgłosić IOException. To sygnał dla użytkownika metody: „Bądź gotów obsłużyć błąd odczytu pliku!”
Dokumentowanie wyjątków: znacznik @throws w Javadoc
Aby inni programiści rozumieli, jakie wyjątki może zgłosić twoja metoda, używaj znacznika @throws (lub @exception) w Javadoc.
Przykład:
/**
* Odczytuje zawartość pliku.
*
* @param filename nazwa pliku
* @return zawartość pliku jako String
* @throws IOException jeśli wystąpił błąd odczytu pliku
*/
public String readFile(String filename) throws IOException {
// ...
}
Po co to robić?
- Pomaga innym programistom zrozumieć, jakie błędy należy obsłużyć.
- IDE i generatory dokumentacji (np. Javadoc) pokazują te wyjątki bezpośrednio w podpowiedziach.
- Zwiększa niezawodność i przewidywalność kodu.
Wyjątki checked i unchecked w API
Wyjątki checked (pochodne Exception, ale nie RuntimeException) są częścią kontraktu metody. Należy je albo obsłużyć, albo jawnie przekazać dalej (throws).
Wyjątki unchecked (RuntimeException i pochodne) zwykle sygnalizują błędy programistyczne (np. NullPointerException, IllegalArgumentException). Nie trzeba ich wskazywać w sygnaturze, ale jeśli twoja metoda może taki wyjątek zgłosić (np. przy niepoprawnych argumentach), warto to również opisać w Javadoc.
Przykład:
/**
* Dzieli a przez b.
* @param a dzielna
* @param b dzielnik
* @return wynik dzielenia
* @throws IllegalArgumentException jeśli b == 0
*/
public int divide(int a, int b) {
if (b == 0) throw new IllegalArgumentException("Dzielnik nie może być zerem");
return a / b;
}
Wyjątki a projektowanie API
- Myśl o użytkowniku swojej metody: jakie błędy może i powinien obsłużyć? Które to błędy programistyczne, a które – „normalne” sytuacje?
- Nie nadużywaj wyjątków checked: jeśli błąd to błąd programistyczny (np. nieprawidłowy argument), lepiej zgłaszać wyjątek unchecked.
- Dokumentuj wszystkie wyjątki, które mogą zostać propagowane wyżej.
2. Konstrukcja try-with-resources
Problem: jak bezpiecznie zamykać zasoby?
W wielu zadaniach pracujemy z zasobami, które trzeba koniecznie zamykać po użyciu: pliki, połączenia sieciowe, bazy danych itd. Jeśli zapomnisz zamknąć zasób – możesz doprowadzić do wycieku pamięci, zablokowania pliku lub innych problemów.
Dawniej:
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("data.txt"));
String line = reader.readLine();
// ...
} catch (IOException e) {
// obsługa błędu
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
// obsługa błędu podczas zamykania
}
}
}
Dużo kodu, łatwo o błąd, można zapomnieć zamknąć zasób.
Rozwiązanie: try-with-resources
Od Javy 7 dostępna jest konstrukcja try-with-resources, która automatycznie zamyka wszystkie zasoby, nawet jeśli wewnątrz bloku wystąpi wyjątek.
Składnia:
try (ResourceType resource = new ResourceType(...)) {
// praca z zasobem
} catch (ExceptionType e) {
// obsługa błędu
}
Przykład:
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("Błąd podczas odczytu pliku: " + e.getMessage());
}
// reader.close() zostanie wywołane automatycznie!
Jak to działa?
- W nawiasach po try deklaruje się zasoby, które należy zamknąć.
- Po wyjściu z bloku try (nawet jeśli wystąpił wyjątek!) dla każdego zasobu wywoływana jest metoda close().
- Działa to tylko dla zasobów implementujących interfejs AutoCloseable (lub jego przodek Closeable).
Interfejs AutoCloseable:
public interface AutoCloseable {
void close() throws Exception;
}
Wszystkie standardowe zasoby Javy (pliki, strumienie, połączenia z bazą danych) implementują ten interfejs.
Można deklarować wiele zasobów
try (
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))
) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
}
Oba zasoby zostaną zamknięte automatycznie, nawet jeśli wystąpi wyjątek.
Zalety try-with-resources
- Bezpieczeństwo: zasoby są zawsze zamykane, nawet przy błędach.
- Zwięzłość: mniej kodu, mniejsze ryzyko pomyłki.
- Czytelność: od razu widać, jakie zasoby są używane i kiedy są zamykane.
3. Praktyka: piszemy bezpieczny kod z try-with-resources
Przykład: odczyt pliku
public static void printFirstLine(String filename) {
try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
String line = reader.readLine();
System.out.println("Pierwsza linia: " + line);
} catch (IOException e) {
System.out.println("Błąd: " + e.getMessage());
}
}
Przykład: zapis do pliku
public static void writeToFile(String filename, String text) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) {
writer.write(text);
} catch (IOException e) {
System.out.println("Błąd podczas zapisu: " + e.getMessage());
}
}
Przykład: własny zasób
Jeśli tworzysz własną klasę, którą należy zamykać, po prostu zaimplementuj AutoCloseable:
public class MyResource implements AutoCloseable {
@Override
public void close() {
System.out.println("Zasób zamknięty!");
}
}
Teraz można go używać w try-with-resources:
try (MyResource res = new MyResource()) {
// praca z zasobem
}
4. Typowe błędy i najlepsze praktyki
Błąd nr 1: zapomniano zamknąć zasób (bez try-with-resources).
Jeśli nie używasz try-with-resources, łatwo zapomnieć zamknąć plik lub strumień – prowadzi to do wycieków zasobów.
Błąd nr 2: próba użycia try-with-resources z obiektem, który nie implementuje AutoCloseable.
Jeśli twoja klasa nie implementuje tego interfejsu, kompilator nie pozwoli użyć jej w try-with-resources.
Błąd nr 3: brak dokumentowania wyjątków w API.
Jeśli twoja metoda może zgłosić wyjątek – koniecznie wskaż to w sygnaturze (throws) i w Javadoc (@throws). To pomoże innym poprawnie korzystać z twojego kodu.
Błąd nr 4: łapanie Exception zamiast konkretnych wyjątków.
Lepiej łapać tylko te wyjątki, których rzeczywiście się spodziewasz i które potrafisz obsłużyć.
GO TO FULL VERSION