1. Hierarchia wyjątków wejścia/wyjścia
W Javie przy pracy z plikami i innymi zewnętrznymi zasobami niemal zawsze masz do czynienia z tzw. wyjątkami wejścia/wyjścia. To obiekty, które są zgłaszane (throw) w przypadku, gdy coś poszło nie tak podczas odczytu lub zapisu danych. Na przykład gdy pliku nie znaleziono, brak dostępu albo dysk nagle „się zmęczył”.
Główni bohaterowie naszego wykładu
W Javie istnieje cała hierarchia takich wyjątków. Oto najważniejsze z nich:
- IOException — klasa bazowa dla wszystkich błędów wejścia/wyjścia. Jeśli coś poszło nie tak z plikami, strumieniami lub siecią — niemal zawsze „winny” jest on (albo któryś z jego licznych potomków).
- FileNotFoundException — „syn” IOException, który pojawia się, gdy próbujesz otworzyć plik, który nie istnieje, lub ścieżka jest podana nieprawidłowo.
Inne pochodne:
- EOFException — również bezpośredni „syn” IOException. Sygnałuje, że podczas odczytu niespodziewanie dotarliśmy do końca pliku.
- MalformedInputException — „wnuk”: dziedziczy po CharacterCodingException, który z kolei dziedziczy po IOException. Taki błąd pojawia się, gdy pliku nie można poprawnie zinterpretować w zadanym kodowaniu (np. oczekiwano "UTF-8", a dotarła uszkodzona sekwencja).
- Są też SocketException, ZipException i inni wyspecjalizowani „krewni”, każdy ze swoim zakresem odpowiedzialności. Im głębiej w hierarchię, tym węższa i bardziej specyficzna sytuacja.
Mały uproszczony schemat:
java.lang.Exception
└── java.io.IOException
├── java.io.FileNotFoundException
├── java.io.EOFException
├── java.nio.charset.MalformedInputException
└── ... (inne)
Ciekawostka:
W Javie prawie wszystkie operacje na plikach wymagają deklarowania lub obsługi IOException — to tzw. wyjątki kontrolowane. Kompilator nie pozwoli ci zapomnieć o obsłudze błędów!
2. Kiedy i dlaczego pojawiają się te wyjątki
Otwarcie nieistniejącego pliku
Najczęstszy przypadek — próbujesz otworzyć plik, a jego nie ma. To jak przyjść na przystanek, a autobusu nie ma nawet w rozkładzie.
FileInputStream fis = new FileInputStream("abracadabra.txt"); // Bach! FileNotFoundException
Próba zapisu do pliku bez uprawnień
Jeśli próbujesz zapisać plik w katalogu, do którego nie masz uprawnień, otrzymasz FileNotFoundException lub IOException (w zależności od sytuacji).
FileOutputStream fos = new FileOutputStream("/system/secret.txt"); // Bach! FileNotFoundException lub IOException
Błędy odczytu/zapisu z powodu uszkodzenia nośnika
Czasem plik niby istnieje, ale dysk jest uszkodzony, plik jest zajęty przez inny program albo nagle wyłączono prąd — wtedy dostaniesz IOException z różnymi komunikatami.
Inne przyczyny
- Plik jest otwarty tylko do odczytu, a ty próbujesz pisać.
- Ścieżka do pliku jest zbyt długa lub zawiera niedozwolone znaki.
- Plik jest używany przez inny proces.
- Dysk jest pełny.
- Plik został usunięty przez inny proces między sprawdzeniem a użyciem.
3. Obsługa za pomocą try-catch
Za każdym razem, gdy pracujesz z plikami, strumieniami, siecią — używaj try-catch. To jak poduszka powietrzna: jeśli coś pójdzie nie tak, program się nie wywróci, tylko zareaguje w kontrolowany sposób.
Jak prawidłowo łapać wyjątki?
W Javie bardziej konkretne wyjątki należy łapać wcześniej niż ogólne. Jeśli ogólny catch (IOException e) umieścisz jako pierwszy, to węższe, np. FileNotFoundException, po prostu nie zadziałają — sterowanie do nich nie dotrze.
Prawidłowa struktura:
try {
// Praca z plikiem
} catch (FileNotFoundException e) {
// Obsługa sytuacji "plik nie znaleziony"
} catch (IOException e) {
// Obsługa innych błędów wejścia/wyjścia
}
Dlaczego tak?
Ponieważ FileNotFoundException to szczególny przypadek IOException. Jeśli złapiesz przypadek ogólny wcześniej, to szczegółowy „nie dotrze” do swojego catch.
Przykład kodu: obsługa błędów przy otwieraniu pliku
import java.io.*;
public class FileReaderExample {
public static void main(String[] args) {
String filename = "notes.txt";
try {
BufferedReader reader = new BufferedReader(new FileReader(filename));
String line = reader.readLine();
System.out.println("Pierwsza linia pliku: " + line);
reader.close();
} catch (FileNotFoundException e) {
System.out.println("Nie znaleziono pliku: " + filename);
} catch (IOException e) {
System.out.println("Błąd podczas odczytu pliku: " + e.getMessage());
}
}
}
Zwróć uwagę:
Nawet jeśli sprawdziłeś, że plik istnieje, zawsze zostawiaj try-catch — plik może zniknąć w każdej chwili (np. usunie go inny proces).
4. Praktyka: piszemy kod z obsługą błędów
Stworzymy proste przykłady i sprawimy, by poprawnie reagowały na brak pliku i inne błędy.
Krok 1: Spróbujmy otworzyć nieistniejący plik
import java.io.*;
public class NotesApp {
public static void main(String[] args) {
String filename = "my_notes.txt";
try {
BufferedReader reader = new BufferedReader(new FileReader(filename));
String line;
System.out.println("Twoje notatki:");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
} catch (FileNotFoundException e) {
System.out.println("Oj! Nie znaleziono pliku z notatkami: " + filename);
System.out.println("Wskazówka: utwórz plik lub sprawdź nazwę.");
} catch (IOException e) {
System.out.println("Wystąpił błąd podczas odczytu pliku: " + e.getMessage());
}
}
}
Krok 2: Dodajmy obsługę zapisu pliku
import java.io.*;
public class NotesWriter {
public static void main(String[] args) {
String filename = "my_notes.txt";
try {
BufferedWriter writer = new BufferedWriter(new FileWriter(filename, true)); // append: true
writer.write("Nowa notatka!\n");
writer.close();
System.out.println("Notatka została pomyślnie dodana!");
} catch (IOException e) {
System.out.println("Błąd podczas zapisu do pliku: " + e.getMessage());
}
}
}
Krok 3: Uniwersalny przykład z logowaniem błędów
import java.io.*;
public class SafeFileCopier {
public static void main(String[] args) {
String source = "source.txt";
String target = "target.txt";
try {
BufferedReader reader = new BufferedReader(new FileReader(source));
BufferedWriter writer = new BufferedWriter(new FileWriter(target));
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
reader.close();
writer.close();
System.out.println("Plik skopiowano pomyślnie!");
} catch (FileNotFoundException e) {
System.out.println("Nie znaleziono pliku: " + e.getMessage());
} catch (IOException e) {
System.out.println("Błąd wejścia/wyjścia: " + e.getMessage());
}
}
}
Wskazówka:
W rzeczywistych aplikacjach warto logować błędy nie tylko na ekran, ale też do pliku lub dziennika, aby później móc ustalić, co dokładnie poszło nie tak.
5. Tabela: najważniejsze wyjątki wejścia/wyjścia
| Wyjątek | Kiedy występuje? | Jak obsłużyć? |
|---|---|---|
|
Nie znaleziono pliku, ścieżka nie istnieje, brak uprawnień | Poinformować użytkownika, sprawdzić ścieżkę/uprawnienia, w razie potrzeby utworzyć plik |
|
Nieoczekiwany koniec pliku podczas odczytu | Poinformować o uszkodzeniu/niekompletności, spróbować częściowo odzyskać dane |
|
Ogólny błąd wejścia/wyjścia (dysk, uprawnienia, blokada) | Sprawdzić szczegóły, poprawnie zakończyć operację, w miarę możliwości ponowić próbę |
|
Nieprawidłowe kodowanie lub struktura pliku | Poinformować o uszkodzeniu, spróbować innego kodowania/źródła |
6. Typowe błędy przy obsłudze wyjątków wejścia/wyjścia
Błąd nr 1: Łapanie tylko ogólnego Exception. Bardzo kuszące jest napisać po prostu catch (Exception e), ale wtedy nie rozróżnisz, co dokładnie poszło nie tak. Lepiej najpierw łapać konkretne wyjątki (FileNotFoundException), a potem — ogólny IOException.
Błąd nr 2: Nie zamykanie strumieni przy błędach. Jeśli otworzyłeś plik, a potem wystąpił wyjątek, strumień może pozostać niezamknięty. Używaj try-with-resources lub zamykaj zasoby w finally.
Błąd nr 3: Ignorowanie komunikatów wyjątków. Nie wypisuj po prostu „Błąd!”, tylko pokazuj szczegóły: e.getMessage(). To pomoże szybciej zrozumieć, co poszło nie tak.
Błąd nr 4: Brak obsługi FileNotFoundException przy zapisie. Wiele osób myśli, że FileNotFoundException dotyczy tylko odczytu. W rzeczywistości może wystąpić także przy zapisie (nieprawidłowa ścieżka, brak uprawnień do utworzenia pliku itd.).
Błąd nr 5: Niesprawdzanie uprawnień dostępu. Jeśli program uruchamiany jest z ograniczonymi uprawnieniami, wiele operacji na plikach może zakończyć się błędem. Zawsze bierz to pod uwagę i informuj użytkownika.
GO TO FULL VERSION