1. Wprowadzenie
Praca z plikami w Javie (i nie tylko!) — to zawsze praca z zewnętrznymi zasobami. Gdy otwierasz plik, system operacyjny przydziela twojemu programowi „deskryptor” — specjalny identyfikator, który pozwala czytać i pisać do pliku. Liczba takich deskryptorów jest ograniczona: jeśli nie zamykasz plików, program może szybko „zjeść” wszystkie dostępne zasoby i zacząć wyrzucać zagadkowe błędy w stylu "Too many open files".
Co więcej, jeśli plik nie jest zamknięty, może pozostać zablokowany dla innych programów. Na przykład otworzyłeś plik do zapisu, zapomniałeś go zamknąć i teraz ani ty, ani nikt inny nie może go zmienić ani usunąć. Taki sobie „wieczny zakładnik” w świecie systemów plików.
Przykład z życia
FileInputStream fis = new FileInputStream("data.txt");
int b = fis.read();
// ... coś robimy, a potem zapomnieliśmy o fis.close()
Jeśli nie wywołasz metody close(), plik pozostanie „wiszący” aż do zakończenia działania programu. W dużych aplikacjach może to prowadzić do wycieków zasobów, a nawet do awarii aplikacji.
2. Stary sposób: finally + close()
Do Javy 7 klasyczny sposób gwarantowanego zamknięcia pliku wyglądał tak:
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
// czytamy plik
int b = fis.read();
// ...
} catch (IOException e) {
System.out.println("Błąd podczas odczytu pliku: " + e.getMessage());
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
System.out.println("Błąd podczas zamykania pliku: " + e.getMessage());
}
}
}
Wady takiego podejścia
- Łatwo zapomnieć o bloku finally i doprowadzić do wycieku zasobów.
- Dużo zbędnego kodu, zwłaszcza gdy strumieni jest kilka.
- Jeśli podczas zamykania wystąpi wyjątek, trzeba go również łapać oddzielnie.
- Kod staje się ociężały i mniej czytelny.
3. Nowoczesne podejście: try-with-resources
Na szczęście w Javie 7 pojawiła się składnia, która rozwiązuje te problemy elegancko i automatycznie — try-with-resources.
Jak to wygląda
try (FileInputStream fis = new FileInputStream("data.txt")) {
int b = fis.read();
// pracujemy z plikiem
} catch (IOException e) {
System.out.println("Błąd podczas pracy z plikiem: " + e.getMessage());
}
// Tutaj fis jest już automatycznie zamknięty!
Kluczowa zaleta: wszystkie zasoby zadeklarowane w nawiasach po try zostaną automatycznie zamknięte po zakończeniu bloku — nawet jeśli po drodze wystąpi wyjątek. Nie trzeba pisać finally, nie trzeba osobno łapać błędów zamykania — Java zrobi to za ciebie.
Jakie klasy można używać w try-with-resources?
Dowolna klasa, która implementuje interfejs AutoCloseable (lub stary, poczciwy Closeable). To prawie wszystkie standardowe strumienie wejścia/wyjścia: FileInputStream, FileOutputStream, BufferedReader, BufferedWriter, Scanner, PrintWriter i wiele innych.
4. Składnia try-with-resources: szczegóły i przykłady
Jeden zasób
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("Błąd: " + e.getMessage());
}
// reader jest zamknięty automatycznie!
Wiele zasobów
Można zadeklarować kilka zasobów oddzielonych średnikiem:
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();
}
} catch (IOException e) {
System.out.println("Błąd podczas kopiowania: " + e.getMessage());
}
// oba strumienie są zamknięte!
Kolejność zamykania: zasoby są zamykane w odwrotnej kolejności do ich deklaracji. Najpierw wywoła się writer.close(), a następnie reader.close(). To ważne, jeśli jeden strumień zależy od drugiego.
Użycie z klasami własnymi
Jeśli piszesz własną klasę, która pracuje z zasobami, po prostu zaimplementuj interfejs AutoCloseable:
class MyResource implements AutoCloseable {
public void doSomething() {
System.out.println("Pracujemy z zasobem!");
}
@Override
public void close() {
System.out.println("Zasób został zamknięty!");
}
}
try (MyResource res = new MyResource()) {
res.doSomething();
}
// Po wyjściu z bloku zostanie wypisane: "Zasób został zamknięty!"
5. Jak to działa: schemat
flowchart TD
A[Otwarcie zasobu w try-with-resources] --> B{W bloku try wystąpił wyjątek?}
B -- Nie --> C[Zasób zamyka się automatycznie]
B -- Tak --> D[Zasób zamyka się automatycznie]
D --> E[Wyjątek jest propagowany dalej]
C --> F[Program kontynuuje wykonywanie]
Wniosek: niezależnie od tego, czy wystąpił błąd, zasób zawsze zostanie zamknięty!
6. Przykłady: przepisujemy kod „na nowo”
Było (stary styl):
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("input.txt"));
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("Błąd: " + e.getMessage());
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
System.out.println("Błąd przy zamykaniu: " + e.getMessage());
}
}
}
Po zmianie (try-with-resources):
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("Błąd: " + e.getMessage());
}
// To wszystko, żadnego finally!
7. Co się dzieje, gdy wystąpi błąd podczas zamykania?
Czasem sama operacja zamykania zasobu może wyrzucić wyjątek (na przykład, gdy plik nagle zniknął). W try-with-resources takie wyjątki nie giną: jeśli w bloku try wystąpił już jeden wyjątek, a przy zamykaniu zasobu pojawił się drugi, zostanie on dodany jako „stłumiony” (suppressed exception) do głównego. Można to zobaczyć za pomocą metody Throwable.getSuppressed().
Przykład
try (MyResource res = new MyResource()) {
throw new IOException("Błąd w bloku try");
} catch (IOException e) {
System.out.println("Błąd główny: " + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("Wyjątek stłumiony: " + suppressed.getMessage());
}
}
8. Jakie klasy obsługują try-with-resources?
To proste: każda klasa implementująca interfejs AutoCloseable. Oto tylko niektóre ze standardowych:
| Klasa | Przeznaczenie |
|---|---|
|
Odczyt bajtów z pliku |
|
Zapis bajtów do pliku |
|
Odczyt/zapis tekstu |
|
Buforowanie strumieni |
|
Zapis tekstu z formatowaniem |
|
Odczyt danych z pliku/konsoli |
|
Serializacja/deserializacja |
|
Praca z archiwami ZIP |
|
Połączenia sieciowe |
Jeśli używasz bibliotek zewnętrznych — sprawdź dokumentację: jeśli istnieje metoda close(), najprawdopodobniej klasa obsługuje try-with-resources.
9. Wskazówki i przydatne niuanse
Można deklarować zmienne poza try (od Java 9): można użyć już zadeklarowanych zasobów, jeśli są final lub „effectively final”:
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try (reader) {
// ...
}
Działa nie tylko z plikami: try-with-resources jest wygodne dla wszelkich zasobów: połączenia sieciowe, bazy danych, dowolne obiekty z metodą close().
Nie ignoruj wyjątków: nawet z try-with-resources nie zapominaj łapać i obsługiwać wyjątków — to nie panaceum na wszystkie problemy, a jedynie wygodny sposób unikania wycieków.
Nie zamykaj zasobu ręcznie wewnątrz try: to niepotrzebne — Java zrobi to za ciebie! Jeśli wywołasz close() ręcznie, a potem zakończy się blok try, nastąpi próba zamknięcia już zamkniętego zasobu. Zwykle jest to bezpieczne, ale może wprowadzać w błąd.
10. Typowe błędy przy użyciu try-with-resources
Błąd nr 1: w ogóle nie użyto try-with-resources. Jeśli wciąż piszesz finally { resource.close(); } — albo wciąż żyjesz w roku 2011, albo nie czytałeś(-aś) tej lekcji! Używaj nowoczesnej składni.
Błąd nr 2: zasób zadeklarowano poza try, a wewnątrz tylko użyto. Taki kod nie zamknie zasobu automatycznie:
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try {
// ... używamy reader
} finally {
// A tutaj zapomnieliśmy zamknąć!
}
Błąd nr 3: wywołujesz close() ręcznie wewnątrz bloku try. To nie jest krytyczne, ale zbędne i może prowadzić do podwójnego zamykania. Po prostu zaufaj Javie.
Błąd nr 4: łapiesz tylko Exception, ignorując specyfikę IO. Lepiej łapać konkretne wyjątki (FileNotFoundException, IOException), aby przekazywać użytkownikowi zrozumiałe komunikaty.
Błąd nr 5: nie obsługujesz stłumionych wyjątków. Jeśli podczas zamykania zasobu wystąpił błąd, może on zostać „stłumiony”. Jeśli analizujesz błędy, nie zapominaj o getSuppressed().
GO TO FULL VERSION