CodeGym /Kursy /JAVA 25 SELF /Praca z try-with-resources: automatyczne zamykanie zasobó...

Praca z try-with-resources: automatyczne zamykanie zasobów

JAVA 25 SELF
Poziom 36, Lekcja 4
Dostępny

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
FileInputStream
Odczyt bajtów z pliku
FileOutputStream
Zapis bajtów do pliku
FileReader/FileWriter
Odczyt/zapis tekstu
BufferedReader/Writer
Buforowanie strumieni
PrintWriter
Zapis tekstu z formatowaniem
Scanner
Odczyt danych z pliku/konsoli
ObjectInputStream/Output
Serializacja/deserializacja
ZipInputStream/Output
Praca z archiwami ZIP
Socket, ServerSocket
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().

1
Zadanie
JAVA 25 SELF, poziom 36, lekcja 4
Niedostępne
Utrwalenie przesłania na kamieniu z magiczną pieczęcią ✨
Utrwalenie przesłania na kamieniu z magiczną pieczęcią ✨
1
Zadanie
JAVA 25 SELF, poziom 36, lekcja 4
Niedostępne
Zliczanie starożytnych manuskryptów w bibliotece 📚
Zliczanie starożytnych manuskryptów w bibliotece 📚
1
Ankieta/quiz
Czytanie i zapisywanie plików, poziom 36, lekcja 4
Niedostępny
Czytanie i zapisywanie plików
Czytanie i zapisywanie plików
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION