Dlaczego exception handling jest kluczowy?
W realnych aplikacjach błędy są nieuniknione – sieć może się zerwać, plik może nie istnieć, użytkownik może wpisać nieprawidłowe dane. Bez prawidłowej obsługi wyjątków Twoja aplikacja będzie crashować przy każdym problemie, a użytkownicy szybko się zniechęcą.
Co się nauczysz:
- Jak działają wyjątki w Javie i dlaczego są niezbędne
- Kiedy używać try-catch, a kiedy throws
- Różnica między checked i unchecked exceptions
- Jak tworzyć własne wyjątki dla specyficznych przypadków
- Best practices które uratują Ci życie w produkcji
- Jak debugować aplikacje używając informacji z wyjątków
Czym są wyjątki i jak działają?
Wyjątek (exception) to sygnał, że coś poszło nie tak podczas wykonywania programu. To nie oznacza, że program musi się skończyć – możemy „złapać” wyjątek i odpowiednio zareagować.
public class BasicExceptionExample { public static void main(String[] args) { // To spowoduje wyjątek - dzielenie przez zero int wynik = 10 / 0; // ArithmeticException! System.out.println("Wynik: " + wynik); // Ta linia nigdy się nie wykona } }
Ten kod crashuje aplikację. Zobaczmy jak to naprawić:
public class SafeDivisionExample { public static void main(String[] args) { try { int wynik = 10 / 0; System.out.println("Wynik: " + wynik); } catch (ArithmeticException e) { System.out.println("Błąd: Nie można dzielić przez zero!"); System.out.println("Szczegóły: " + e.getMessage()); } System.out.println("Program działa dalej!"); // Ta linia się wykona } }
Try-catch-finally – Twoja linia obrony
Blok try-catch-finally to podstawowy mechanizm obsługi wyjątków:
import java.io.*; public class FileHandlingExample { public static void readFile(String fileName) { FileReader fileReader = null; BufferedReader bufferedReader = null; try { // Kod który może wywołać wyjątek fileReader = new FileReader(fileName); bufferedReader = new BufferedReader(fileReader); String line; while ((line = bufferedReader.readLine()) != null) { System.out.println(line); } } catch (FileNotFoundException e) { // Konkretny wyjątek - plik nie istnieje System.out.println("Plik nie został znaleziony: " + fileName); } catch (IOException e) { // Ogólniejszy wyjątek - problemy z I/O System.out.println("Błąd podczas czytania pliku: " + e.getMessage()); } finally { // Sprzątanie - wykonuje się ZAWSZE try { if (bufferedReader != null) { bufferedReader.close(); } if (fileReader != null) { fileReader.close(); } } catch (IOException e) { System.out.println("Błąd podczas zamykania plików"); } } } }
Checked vs Unchecked exceptions
Java dzieli wyjątki na dwie kategorie:
Checked Exceptions | Unchecked Exceptions |
---|---|
Musisz je obsłużyć lub przekazać dalej | Możesz je obsłużyć, ale nie musisz |
IOException, SQLException, ClassNotFoundException | RuntimeException, NullPointerException, ArrayIndexOutOfBoundsException |
Kompilator zmusi Cię do reakcji | Kompilator pozwoli na ignorowanie |
Zazwyczaj zewnętrzne problemy | Zazwyczaj błędy programisty |
public class ExceptionTypesExample { // Checked exception - musisz je obsłużyć public void readFileMethod() throws IOException { FileReader file = new FileReader("test.txt"); // IOException - checked! } // Unchecked exception - możesz obsłużyć public void riskyMethod() { String text = null; int length = text.length(); // NullPointerException - unchecked! } public static void main(String[] args) { ExceptionTypesExample example = new ExceptionTypesExample(); // Dla checked exception musisz użyć try-catch lub throws try { example.readFileMethod(); } catch (IOException e) { System.out.println("Problem z plikiem: " + e.getMessage()); } // Dla unchecked możesz, ale nie musisz try { example.riskyMethod(); } catch (NullPointerException e) { System.out.println("Ups, null pointer!"); } } }
Throws – przekazywanie odpowiedzialności
Czasami nie chcesz obsługiwać wyjątku w danej metodzie, tylko przekazać go „wyżej”:
public class DatabaseService { // Ta metoda nie obsługuje SQLException, tylko go przekazuje public void connectToDatabase() throws SQLException { DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb"); // Jeśli connection się nie uda, SQLException poleci wyżej } // Ta metoda łapie wyjątek i podejmuje konkretną akcję public boolean tryConnect() { try { connectToDatabase(); System.out.println("Połączono z bazą danych!"); return true; } catch (SQLException e) { System.out.println("Nie można połączyć z bazą: " + e.getMessage()); // Może spróbuj backup database? return tryBackupDatabase(); } } private boolean tryBackupDatabase() { System.out.println("Próbuję połączyć z backup database..."); return false; // Implementacja dla przykładu } }
Tworzenie własnych wyjątków
Czasami standardowe wyjątki nie wystarczają. Możesz tworzyć własne:
// Własny wyjątek dla błędów biznesowych public class InsufficientFundsException extends Exception { private double balance; private double requestedAmount; public InsufficientFundsException(double balance, double requestedAmount) { super("Niewystarczające środki. Saldo: " + balance + ", próba wypłaty: " + requestedAmount); this.balance = balance; this.requestedAmount = requestedAmount; } public double getBalance() { return balance; } public double getRequestedAmount() { return requestedAmount; } } public class BankAccount { private double balance; public BankAccount(double initialBalance) { this.balance = initialBalance; } public void withdraw(double amount) throws InsufficientFundsException { if (amount > balance) { // Rzucamy nasz własny wyjątek z konkretnymi danymi throw new InsufficientFundsException(balance, amount); } balance -= amount; System.out.println("Wypłacono: " + amount + ", saldo: " + balance); } public static void main(String[] args) { BankAccount account = new BankAccount(100.0); try { account.withdraw(50.0); // OK account.withdraw(80.0); // Problem! } catch (InsufficientFundsException e) { System.out.println("Błąd: " + e.getMessage()); System.out.println("Dostępne środki: " + e.getBalance()); } } }
Best practices – co robić a czego unikać
### ✅ Dobre praktyki:
// 1. Łap konkretne wyjątki, nie Exception try { // ryzykowny kod } catch (FileNotFoundException e) { // konkretna reakcja na missing file } catch (IOException e) { // reakcja na inne I/O problemy } // 2. Loguj wyjątki z kontekstem try { processUser(userId); } catch (UserNotFoundException e) { logger.error("Nie znaleziono użytkownika o ID: " + userId, e); throw new ServiceException("Nie można przetworzyć użytkownika", e); } // 3. Używaj zasobów bezpiecznie try (FileInputStream fis = new FileInputStream("file.txt")) { // Java 7+ automatycznie zamknie plik } catch (IOException e) { handleError(e); }
### ❌ Złe praktyki:
// 1. NIE rób tego - "łykanie" wyjątków try { riskyOperation(); } catch (Exception e) { // Milczenie = śmierć aplikacji w produkcji! } // 2. NIE łap Exception - za ogólne try { complexOperation(); } catch (Exception e) { // Co się stało? Nie wiadomo! } // 3. NIE używaj wyjątków do kontroli flow try { String value = map.get(key); if (value == null) { throw new MyException("No value"); // Źle! } } catch (MyException e) { // używaj if-else, nie exceptions }
Debugging z pomocą wyjątków
Wyjątki to Twoi najlepsi przyjaciele podczas debugowania:
public class DebuggingExample { public static void main(String[] args) { try { methodA(); } catch (Exception e) { // Wypisz pełny stack trace e.printStackTrace(); // Albo loguj strukturalnie System.out.println("Błąd: " + e.getMessage()); System.out.println("Typ: " + e.getClass().getSimpleName()); System.out.println("Lokalizacja: " + e.getStackTrace()[0]); } } static void methodA() { methodB(); } static void methodB() { methodC(); } static void methodC() { // Tu nastąpi błąd String text = null; text.length(); // NullPointerException } }
Stack trace pokaże Ci dokładnie:
– Gdzie wystąpił błąd (methodC, linia X)
– Jak tam dotarłeś (methodA → methodB → methodC)
– Jaki to był błąd (NullPointerException)
Checked dla sytuacji których programista nie kontroluje (problemy sieci, I/O, zewnętrzne API). Unchecked dla błędów programisty (null pointers, złe argumenty). Jeśli klient może sensownie zareagować na błąd – checked. Jeśli to bug do naprawienia – unchecked.
Tak! try-finally bez catch jest OK – finally wykona się nawet jeśli wyjątek „przepłynie” wyżej. Przydatne do sprzątania zasobów gdy nie chcesz obsługiwać błędu lokalnie.
Wyjątek z finally „przysłoni” oryginalny wyjątek z try. To rzadki ale podstępny problem. Zawsze owijaj ryzykowny kod w finally w dodatkowy try-catch.
Rzucanie wyjątku jest kosztowne (stack trace), ale try-catch bez wyjątku ma minimalny koszt. Nie używaj wyjątków do normalnego flow kontroli, ale nie bój się ich dla rzeczywistych błędów.
Zawsze końcówka „Exception”: UserNotFoundException, PaymentFailedException. Nazwa powinna jasno mówić co poszło nie tak. Grupuj w hierarchie: ServiceException → UserServiceException.
Try-catch poza pętlą jest lepsze niż wewnątrz, ale nie zawsze możliwe. Jeśli każda iteracja może rzucić różne wyjątki, trzymaj try-catch w pętli. Testuj wydajność w konkretnym przypadku.
Tak! To częsta i dobra praktyka. Jeśli nie można utworzyć obiektu w poprawnym stanie, rzuć wyjątek. Obiekt nie zostanie utworzony i pamięć będzie zwolniona przez GC.
Przydatne zasoby:
🚀 Zadanie dla Ciebie
Napisz klasę FileProcessor która:
- Czyta plik tekstowy linia po linii
- Liczy ile linii zawiera słowo „Java”
- Obsługuje wszystkie możliwe błęды: brak pliku, problemy z czytaniem, etc.
- Ma własny wyjątek FileProcessingException
- Zawsze zamyka pliki (nawet przy błędzie)
- Loguje szczegółowe informacje o błędach
Przetestuj z różnymi scenariuszami: plik istnieje, nie istnieje, jest pusty, nie masz uprawnień do odczytu.
Czy masz pytania o konkretne przypadki exception handling w Twoich projektach? Podziel się w komentarzach!