Jako przedstawiciel tej grupy zawodowej
– z pełną odpowiedzialnością muszę przyznać – że programiści z natury są raczej zapominalscy…
Zaowocowało to powstaniem wielu automatycznych rozwiązań, które mają odciążyć nas od pamiętania przynajmniej o niektórych sprawach (np. konstrukcja try with resources). Co jest jak najbardziej wskazaną praktyką, ponieważ możemy się wtedy skupić na ciekawszych i bardziej rozwijających zadaniach.
Przyjrzymy się dzisiaj mechanizmowi automatycznego zamykania zasobów z wykorzystaniem wyrażenia: try with resources.
Spis treści
- 1 Try with resources
- 2 Try catch finally
- 3 Try-with-resources
- 4 try with resources – Jak to działa?
- 5 Własny manager kontekstu z wykorzystaniem interfejsu AutoCloseable
- 6 try with resources – Poprawna obsługa zagnieżdżonych zasobów
- 7 Suppressed Exceptions – zduszone wyjątki
- 8 Podsumowanie – Try With Resources oraz ZADANIE
- 9 20+ BONUSOWYCH materiałów z programowania
Try with resources
Zacznijmy od małego wprowadzenia. Na początku były jakieś zasoby, np. strumień danych lub uchwyt do pliku. Programiści chętnie z nich korzystali. Wielu z nich jednak zapominało, że po skończonej pracy wypadałoby po sobie zwyczajnie posprzątać i zamknąć taki zasób, tak by inni mogli z niego później skorzystać. W efekcie takiego niechlujstwa wiele aplikacji cierpiało na różnego rodzaju wycieki.
Try catch finally
Standardowa procedura z wykorzystaniem konstrukcji try catch finally wygląda mniej więcej tak:
- w bloku try otwieramy zasób oraz korzystamy z niego;
- w bloku catch obsługujemy wyjątki lub przekazujemy je dalej. Blok catch nie jest obowiązkowy i możemy go pominąć przy założeniu, że wyjątki zostaną obsłużone w dalszych warstwach aplikacji;
- w bloku finally sprzątamy po sobie, czyli zamykamy wykorzystywany zasób.
BufferedWriter fileWriter = null; String fileName = "/TryCatchFinally.txt"; try { fileWriter = new BufferedWriter(new FileWriter(fileName)); fileWriter.write("Try Catch Finally."); } catch (IOException e) { e.printStackTrace(); } finally { if (fileWriter != null) { try { fileWriter.close(); } catch (IOException e) { e.printStackTrace(); } } }
I tu tak naprawdę pojawia się problem. ⚠️🚨
Ponieważ, zamknięcie zasobów nie jest w żaden sposób wymagane z punktu widzenia kompilatora Javy i wiele osób zapomina o tym lub robi to źle. Na pierwszy rzut oka zazwyczaj taka aplikacja działa poprawnie, jednak po pewnym czasie dochodzi do dziwnych i trudnych do zdiagnozowania błędów.
Idąc za zasadą, że lepiej zapobiegać niż leczyć, zastanówmy się, jak można zaradzić na takie zapominalstwo.
Try-with-resources
Tutaj całe szczęście przyszli nam z pomocą sami twórcy Javy i od Java 7 mamy natywne wsparcie dla nowej konstrukcji try with resources. Jest to rozbudowana wersja standardowego try catch finally o możliwość automatycznego zarządzania zasobami. Wystarczy, że sami utworzymy taki zasób, następnie Java automatycznie za nas będzie pamiętała, żeby go zamknąć, gdy nie będzie już potrzebny.
try (BufferedWriter fileWriter = new BufferedWriter(new FileWriter(fileName));) { fileWriter.write("Try with resources."); } catch (IOException e) { e.printStackTrace(); }
Prawda, że prościej i czytelniej? I bezpieczniej 🙂
A w skrajnym wypadku, jeżeli zrezygnujemy z obsługi wyjątków możemy ograniczyć się do samej klauzuli try.
try (BufferedWriter fileWriter = new BufferedWriter(new FileWriter(fileName));) { fileWriter.write("Try with resources."); }
ZOBACZ : Dlaczego boilerplate code zabija nasze aplikacje
try with resources – Jak to działa?
Podczas kompilacji try with resources zostaje zamieniony na bytecode, który wygląda bardzo podobnie do tego, jaki byłby wygenerowany na podstawie kodu z pierwszego przykładu (działa tu podobny mechanizm jak np. przy generowaniu domyślnego konstruktora dla klas). Czyli pod spodem nie ma żadnej dodatkowej kosztownej magii, a jedynie zamiana fragmentów kodu podczas kompilacji. Z tego powodu nie ma się co obawiać, że to dodatkowo obciąży naszą aplikację.
Własny manager kontekstu z wykorzystaniem interfejsu AutoCloseable
Żeby móc automatycznie zarządzać zasobami, poza umieszczeniem ich w instrukcji try, muszą one jeszcze implementować interfejs: java.lang.AutoCloseable. Wszystkie klasy obsługujące strumienie wyjścia i wyjścia ze standardowej biblioteki Javy spełniają ten warunek. Dlatego właśnie w poprzednim przykładzie mogliśmy skorzystać z klasy: BufferedReader – implementuje ona pośrednio ten interfejs, klasa BufferedReader dziedziczy po klasie Reader, która implementuje interfejs Closeable, który rozszerza właśnie interfejs AutoCloseable.
Skoro to takie proste, to spróbujmy samodzielnie napisać aplikację z wykorzystaniem konstrukcji try with resources oraz interfejsu AutoCloseable.
Jako przykład niech posłuży nam Matrioszka – to taka rosyjska zabawka, złożona z drewnianych, wydrążonych w środku lalek, włożonych jedna w drugą. Na pierwszy rzut oka niewiele ma to wspólnego z programowaniem, jednak jest to świetny przykład na zagnieżdżenie elementów. Przykładowa struktura lalek zaprezentowana w XML’u mogłaby wyglądać np. tak:
<Big> <Medium> <Small>CoreDoll</Small> </Medium> </Big>
Spróbujmy teraz wygenerować taką strukturę dynamicznie korzystając z automatycznego zarządzania kontekstem.
class MatryoshkaDoll implements AutoCloseable { private final String name; public MatryoshkaDoll(String name) { this.name = name; System.out.println("<" + name + ">"); } @Override public void close() { System.out.println("</" + name + ">"); } } try (MatryoshkaDoll big = new MatryoshkaDoll("Big"); MatryoshkaDoll medium = new MatryoshkaDoll("Medium"); MatryoshkaDoll small = new MatryoshkaDoll("Small")) { System.out.println("CoreDoll"); }
- tworzymy klasę MatryoshkaDoll, która implementuje AutoCloseable;
- w instrukcji try() otwieramy trzy kolejne laleczki (zasoby): big, medium, small;
- w ciele bloku try wywołujemy główną logikę – wyświetlamy laleczkę CoreDoll;
- zasoby zostaną automatycznie zamknięte (wywołanie metody close) w kolejności odwrotnej, do tej, w której były otwierane.
Na tym etapie można by właściwie zakończyć rozważania na temat try with resources i w zdecydowanej większości przypadków to, o czym sobie do tej pory powiedzieliśmy będzie dla Ciebie wystarczające. Niestety czasem sytuacje się komplikuje i nie zawsze wszystko idzie tak, jak powinno, dlatego jeżeli będziesz mieć bardzo skomplikowany przypadek warto wiedzieć, jak sobie z nim radzić.
try with resources – Poprawna obsługa zagnieżdżonych zasobów
Niestety poprawna obsługa zagnieżdżonych zasobów nie jest aż taka oczywista i nawet korzystając z konstrukcji try with resources można zrobić to źle. W efekcie czego w przypadku kłopotów możemy zostać z niezamkniętymi zasobami.
Poniżej jest spreparowany przykład dwóch zagnieżdżonych zasobów: InnerResource, który jest wewnętrznym zasobem oraz OuterResource, który wewnętrznie korzysta z InnerResource. W tym przykładzie każda próba utworzenia zewnętrznego zasobu zakończy się niepowodzeniem.
Nie jest to przykład oderwany od rzeczywistości – dość często bowiem zdarzają się przypadki gdzie trzeba wykorzystać dwa strumienie jednocześnie, np. BufferedWriter oraz BufferedReader.
class InnerResource implements AutoCloseable { public InnerResource() { System.out.println("InnerResource created."); } @Override public void close() { System.out.println("InnerResource closed."); } } class OuterResource implements AutoCloseable { private final InnerResource innerResource; public OuterResource(final InnerResource innerResource) { this.innerResource = innerResource; throw new RuntimeException("Error on create OuterResource."); } @Override public void close() { System.out.println("OuterResource closed."); } }
Nieprawidłowa obsługa zagnieżdżonych zasobów w try with resources
Ponieważ konstruktor OuterResource jako argument przyjmuje obiekt typu InnerResource, bardzo kusząca wydaje się poniższa konstrukcja, jednak…
UWAGA: poniżej jest błędny przykład!
try (OuterResource outer = new OuterResource(new InnerResource())) { System.out.println(outer); } catch (Exception exception) { System.out.println("ERROR: " + exception); }
Dla przypomnienia podczas otwierania zewnętrznego zasobu coś pójdzie nie tak i zostanie zgłoszony wyjątek. Zobacz jak wtedy zachowa się nasz kod:
InnerResource created. ERROR: java.lang.RuntimeException: Error on create OuterResource.
Mimo skorzystania z try with resources wewnętrzny zasób nie został zamknięty!
Jak poprawnie zamykać zasoby?
Prawidłowa obsługa zagnieżdźonych zasóbów w try with resources
Wystarczy, że lekko zmodyfikujemy nasz przykład – rozbijając tworzenie zasobów na dwie niezależne instrukcje, by try with resources było w stanie poprawnie je zamknąć.
try (InnerResource inner = new InnerResource(); OuterResource outer = new OuterResource(inner)) { System.out.println(outer); } catch (Exception exception) { System.out.println("ERROR: " + exception); }
Logi z aplikacji to potwierdzają. Mimo wystąpienia wyjątku podczas tworzenia zewnętrznego zasobu InnerResource zostało poprawnie zamknięte.
InnerResource created. InnerResource closed. ERROR: java.lang.RuntimeException: Error on create OuterResource.
Suppressed Exceptions – zduszone wyjątki
Wprowadzenie bloku try with resources spowodowało konieczność wprowadzenia jeszcze jednego mechanizmu: tłumienia wyjątków (ang. suppressed exceptions).
class Resource implements AutoCloseable { public Resource() { System.out.println("Resource created."); } @Override public void close() { System.out.println("Resource closed."); // throw new RuntimeException("Error on close resource."); } } try (Resource resource = new Resource()) { // throw new RuntimeException("Error"); } catch (Exception e) { System.out.println(String.format("ERROR: %s, \nsuppressed: %s ", e, Arrays.toString(e.getSuppressed()))); }
W tym przykładzie jeżeli wszystko pójdzie dobrze powinniście zobaczyć komunikat o utworzeniu, a następnie zamknięciu zasobu.
Resource created. Resource closed.
Ciekawe rzeczy zaczynają się jednak dziać, gdy coś pójdzie nie tak.
Jeżeli wystąpi wyjątek podczas zamykania zasobu, zostanie on normalnie zgłoszony i złapany.
Resource created. ERROR: java.lang.RuntimeException: Error on close resource., suppressed: []
Podobnie w przypadku gdy poleci wyjątek z ciała bloku try.
Resource created. Resource closed. ERROR: java.lang.RuntimeException: Error, suppressed: []
UWAGA: Jeżeli jednak wystąpią oba wyjątki jednocześnie, to wyjątek z metody close, zostanie stłumiony i będzie dostępny tylko przez metodę: Exception.getSuppressed().
Resource created. ERROR: java.lang.RuntimeException: Error, suppressed: [java.lang.RuntimeException: Error on close resource.]
Podsumowanie – Try With Resources oraz ZADANIE
Automatyczne zarządzanie zasobami przy wykorzystaniu try with resources nie jest niezbędne, jednak jest jedną z tych rzeczy, która tak ułatwia pracę, że zdecydowanie warto dodać ją do swojego warsztatu programisty. Dzięki jej wykorzystaniu nie tylko nasz kod będzie bardziej przejrzysty, ale dodatkowo zmniejszymy prawdopodobieństwo wystąpienia trudnych do zdiagnozowania problemów z niezamkniętymi zasobami.
Na dzisiaj to już wszystko. Spróbuj przećwiczyć tę wiedzę w praktyce, implementując przepisywanie danych z jednego pliku do drugiego z wykorzystaniem try with resources oraz dwóch klas obsługujących zasoby: BufferedReader i BufferedWriter.
Wynikami możesz podzielić się w komentarzu poniżej lub na naszej grupie.
20+ BONUSOWYCH materiałów z programowania
e-book – „8 rzeczy, które musisz wiedzieć, żeby dostać pracę jako programista”,
e-book – „Java Cheat Sheet”,
checklista – „Pytania rekrutacyjne”
i wiele, wiele wiecej!