Exception handling – jak nie zabijać aplikacji

TL;DR: Exception handling w Javie to sposób na eleganckie radzenie sobie z błędami zamiast crashowania aplikacji. Używasz try-catch do łapania błędów, finally do sprzątania, a throws do przekazywania odpowiedzialności. Złote zasady: łap konkretne wyjątki, nie ignoruj błędów, zawsze sprzątaj zasoby.

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
Wymagania wstępne: Podstawy Javy (klasy, metody, dziedziczenie), znajomość IDE jak Eclipse lub IntelliJ IDEA

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ć.

Wyobraź sobie wyjątek jak alarm przeciwpożarowy. Gdy się włączy, nie znaczy to że budynek musi się zawalić – masz procedury jak reagować: ewakuacja, wezwanie straży, sprawdzenie szkód.
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");
            }
        }
    }
}
Pro tip: Od Java 7 możesz użyć try-with-resources, które automatycznie zamyka zasoby. Ale w 2015 roku to jeszcze względnie nowa funkcjonalność, więc warto znać też „klasyczne” podejście.

Checked vs Unchecked exceptions

Java dzieli wyjątki na dwie kategorie:

Checked ExceptionsUnchecked Exceptions
Musisz je obsłużyć lub przekazać dalejMożesz je obsłużyć, ale nie musisz
IOException, SQLException, ClassNotFoundExceptionRuntimeException, NullPointerException, ArrayIndexOutOfBoundsException
Kompilator zmusi Cię do reakcjiKompilator pozwoli na ignorowanie
Zazwyczaj zewnętrzne problemyZazwyczaj 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
    }
}
Używaj throws gdy metoda nie może sensownie obsłużyć wyjątku. Używaj try-catch gdy wiesz co zrobić z błędem.

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ć

Uwaga: Te błędy widzę codziennie w kodzie juniorów i czasami seniorów!

### ✅ 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
}
Typowy błąd początkujących: Łapanie wszystkich wyjątków jednym catch (Exception e) i ignorowanie ich. To jak zaklejenie lampki kontrolnej w samochodzie – problem nadal istnieje, tylko go nie widzisz!

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)

Kiedy używać checked a kiedy unchecked exceptions?

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.

Czy można mieć try bez catch?

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.

Co się stanie jeśli finally też rzuci wyjątek?

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.

Czy wyjątki spowalniają aplikację?

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.

Jak nazwać własne wyjątki?

Zawsze końcówka „Exception”: UserNotFoundException, PaymentFailedException. Nazwa powinna jasno mówić co poszło nie tak. Grupuj w hierarchie: ServiceExceptionUserServiceException.

Co z performance w pętlach z try-catch?

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.

Czy można rzucić wyjątek z konstruktora?

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!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry