Rozdział 11 - Wyjątki - Try with resources

Od wersji Java 1.7 dostępna jest nowa wersja instrukcji try..catch..finally nazywana try-with-resources (try z zasobami). Została ona wprowadzona dla wygody programistów, by zautomatyzować zamykanie różnego rodzaju zasobów takich jak obiekty klas odpowiedzialnych za np. odczytywanie danych z pliku.

Różnicę pomiędzy "standardowym" try..catch..finally oraz try-with-resources zobaczymy na przykładzie programu, którego jedynym zadaniem będzie wypisanie na ekran zawartości pliku znajdującego się na dysku komputera.

Aby odczytać z dysku plik w języku Java, musimy:

  1. Utworzyć obiekt typu File, który będzie skojarzony z plikiem na dysku.
  2. Utworzyć strumień danych, który będzie mógł czytać dane z pliku, na który wskazuje obiekt File. Strumieniem danych w naszych przykładach będzie obiekt klasy FileReader – klasa ta pozwala na czytanie z pliku znak po znaku.
  3. Zamknąć strumień po zakończeniu pracy na nim wywołując jego metodę close.

Dodatkowo, gdy korzystamy z klas strumieni, takich jak FileReader, musimy obsłużyć wyjątki, które mogą wystąpić. Oznacza to, że w naszym programie będziemy musieli skorzystać z try..catch do obsługi potencjalnych sytuacji wyjątkowych. Wyjątkiem bazowym używanym w metodach klas, które mają coś wspólnego z działaniami na plikach, jest IOException (Input/Output Exception), i taki też wyjątek będziemy łapać.

Poniżej znajdują się dwa przykłady – pierwszy korzysta ze "starego" podejścia, natomiast drugi używa nowego mechanizmu try-with-resources. Oba programy mają za zadanie odczytać plik z dysku znak po znaku i wypisać go na ekran. Spójrzmy najpierw na przykład korzystający ze zwykłego try..catch..finally:

Nazwa pliku: CzytaniePlikuTryCatch.java
import java.io.File; // 1
import java.io.FileReader;
import java.io.IOException;

public class CzytaniePlikuTryCatch {
  public static void main(String[] args) {
    File f = new File("C:/programowanie/powitanie.txt"); // 2
    FileReader fileReader = null; // 3

    try {
      fileReader = new FileReader(f); // 4
      int odczytanyZnak;

      while ((odczytanyZnak = fileReader.read()) != -1) { // 5
        System.out.print((char) odczytanyZnak); // 6
      }
    } catch (IOException e) { // 7
      System.out.println(e.getMessage());
    } finally {
      try {
        if (fileReader != null) {
          fileReader.close(); // 8
        }
      } catch (IOException e) { // 9
        System.out.println(
            "Blad podczas zamykania strumienia: " + e.getMessage()
        );
      }
    }
  }
}

Ten program wypisuje na ekran zawartość pliku powitanie.txt znajdującego się w katalogu C:\programowanie:

Witaj Swiecie !

Krótka analiza tego programu:

  1. Klasy do pracy z plikami, z których skorzystamy, znajdują się w pakiecie java.io w Bibliotece Standardowej Java.
  2. Tworzymy obiekt klasy File przekazując jako argument konstruktora lokalizację pliku, z którym ten obiekt będzie skojarzony.
  3. Definiujemy obiekt klasy FileReader, który będzie służył do odczytania pliku. Zmienna fileReader znajduje się przed blokiem try..catch ponieważ będziemy z niej chcieli skorzystać w bloku finally. Gdybyśmy umieścili definicję tej zmiennej wewnątrz bloku try, to zmienna ta byłaby dostępna jedynie w tym bloku.
  4. Tworzymy obiekt typu FileReader, który skojarzony będzie z plikiem, na który wskazuje utworzony wcześniej obiekt typu File. Tworzenie tego obiektu jest w bloku try, ponieważ może zostać rzucony wyjątek FileNotFoundException – gdy plik, na który wskazuje przekazany do konstruktora obiekt typu File, nie zostanie znaleziony.
  5. W warunku pętli wykonujemy dwie operacje: przypisujemy do zmiennej odczytanyZnak wartość zwróconą z metody fileReader.read(), która zwraca kod liczbowy przeczytanego znaku, a następnie porównujemy całość tego wyrażenia do -1. Metoda read zwraca wartość -1 w przypadku, gdy przeczytany został już cały plik – będzie to oznaczało koniec działania pętli.
  6. Jeżeli przeczytany kod znaku nie był liczbą -1, to wypisujemy go na ekran, rzutując go najpierw do wartości typu char. Musimy to zrobić, ponieważ metoda read z poprzedniego punktu zwraca nie faktyczny znak w pliku, lecz jego liczbową reprezentację.
  7. Jeżeli wystąpi wyjątek, to łapiemy go i wypisujemy na ekran komunikat błędu.
  8. Na końcu programu powinniśmy zamknąć obiekt fileReader, który służył do czytania z pliku, za pomocą metody close. Najpierw sprawdzamy, czy fileReader nie jest nullem ponieważ w bloku try mógł wystąpić wyjątek podczas próby utworzenia tego obiektu i mógł on nie zostać zainicjalizowany inną wartością niż null, a na nullowym obiekcie nie chcemy wywołać metody close.
  9. Sama metoda close także może rzucić wyjątek IOException, jeżeli z jakiegoś powodu nie powiedzie się próba zamknięcia zasobu, na której ją wywołujemy. Zauważmy, że w sekcji finally ponownie korzystamy z try..catch, by złapać ewentualny wyjątek związany z próbą zamknięcia obiektu fileReader.

Dlaczego instrukcja fileReader.close(); nie jest częścią głównego bloku try..catch? Jeżeli instrukcja ta byłaby po pętli czytającej plik, a w trakcie czytania wystąpiłby wyjątek, to nigdy nie doszłoby do próby zamknięcia strumienia fileReader. Chcemy mieć pewność, że metoda close zostanie wywołana na rzecz obiektu fileReader niezależnie od tego, czy czytanie pliku się powiodło, czy nie – dlatego instrukcja zamykania jest w sekcji finally.

Porównaj powyższy program z jego drugą wersją, która korzysta z mechanizmu try-with-resources:

Nazwa pliku: CzytaniePlikuTryWithResources.java
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

public class CzytaniePlikuTryWithResources {
  public static void main(String[] args) {
    File f = new File("C:/programowanie/powitanie.txt");

    try (FileReader fileReader = new FileReader(f)) {
      int odczytanyZnak;

      while ((odczytanyZnak = fileReader.read()) != -1) {
        System.out.print((char) odczytanyZnak);
      }
    } catch (IOException e) {
      System.out.println(e.getMessage());
    }
  }
}

Ta wersja jest zdecydowanie krótsza – zauważ, że w ogóle nie ma w niej sekcji finally, a sekcja try ma dodatek w postaci tworzenia obiektu fileReader w nawiasach:

try (FileReader fileReader = new FileReader(f)) {

To właśnie ten fragment świadczy o tym, że jest to try-with-resources. Naszym zasobem w tym przypadku jest strumień fileReader typu FileReader. Tak utworzony obiekt jest dostępny w bloku try – odczytywanie i wypisywanie znaku na ekran nie różni się pomiędzy obydwoma programami.

Ta wersja nie ma sekcji finally, ponieważ obiekt fileReader zostanie automatycznie zamknięty (zostanie na nim wywołana metoda close) po zakończeniu bloku try..catch dla naszej wygody i krótszego kodu.

Nie wszystkie klasy mają metodę close – skąd w takim razie wiemy, które klasy mogą być użyte jako "zasoby" w try-with-resources? Zależy to od tego, czy dana klasa implementuje interfejs Closeable lub AutoCloseable. O interfejsach opowiemy sobie w następnym rozdziale.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Nie musisz podawać swojego imienia, e-mailu, ani strony www, aby opublikować komentarz. Komentarze muszą zostać zatwierdzone, aby były widoczne na stronie.