W językach kompilowanych mechanizmy wejścia-wyjścia opiera się niemal zawsze o koncepcję strumienia (stream), czyli abstrakcyjnego obiektu z którego możemy czytać dane i/lub je do niego zapisywać. Ponieważ strumień jest opakowaniem na jakiś zewnętrzny zasób – plik, połączenie sieciowe, itp. – należy generalnie dbać o jego poprawne i szybkie zamknięcie, gdy nie jest już potrzebny. Dotyczy to także zwłaszcza języków z zarządzaną pamięcią, gdzie osierocony obiekt strumienia może nie być posprzątany przez bardzo długi czas, zajmując zewnętrzne, niezarządzane zasoby systemowe.
Poprawny sposób postępowania z obiektem strumienia, który chciałem dzisiaj omówić, dotyczy konkretnie języka Java, gdyż tam cała sprawa jest co najmniej nietrywialna. Dzieje się tak z trzech powodów:
catch
), albo zadeklarowany jako opuszczający funkcję (throws
). Jest to sprawdzane podczas kompilacji.close
, zamykająca strumień, deklaruje potencjalne wyrzucanie wyjątku IOException
.with
(obecnej np. w C# i Pythonie), która automatycznie posprzątałaby po obiekcie strumienia w momencie opuszczenia jej zasięgu.Brak instrukcji with
sprawia, iż do dyspozycji pozostaje nam wyłącznie try-catch
lub try-finally
. Naiwne zastosowanie któregoś z nich nie daje jednak pożądanych efektów:
Takim efektem byłby na przykład fakt kompilowania się kodu :) W tej wersji jest to jednak niemożliwe (o ile funkcja nie deklaruje wyrzucania IOException
), bowiem wyjątek ten może zostać rzucony przez metodę close
… A przynajmniej taka jest teoria, którą kompilator niestety pedantycznie sprawdza.
W rzeczywistości ten kod ma przynajmniej jeszcze jeden błąd, którego nie wyeliminuje otoczenie wywołania close
odpowiednim blokiem try-catch
. Jego znalezienie pozostawiam aczkolwiek jako – ahem – ćwiczenie dla czytelnika ;) W zamian pokażę dla odmiany nieco lepszy sposób na obejście zaprezentowanych problemów.
Polega on na zastosowaniu dwóch zagnieżdżonych bloków try
: jednego z catch
do złapania IOException
i drugiego z finally
do zamknięcia strumienia. W całości prezentuje się to następująco:
Przy zastosowaniu takiej konstrukcji wszystkie miejsca, w których wyjątek I/O może wystąpić, są otoczone blokiem try-catch
, więc kompilator nie będzie miał powodów do narzekań. Nadal też gwarantujemy, że strumień zostanie zawsze zamknięty, co z kolei zapewnia blok try-finally
.
A że wygląda to wszystko cokolwiek nieestetycznie? Cóż… Java :)
Co do “ćwiczenia dla czytelnika”, to chyba to będzie jakimś tam rozwiązaniem: http://download.java.net/jdk7/archive/b123/docs/api/java/lang/Throwable.html#getSuppressed%28%29.
Ten jeszcze jeden błąd to po prostu zmienna w innym zasięgu.
@SebaS86: Problem w tym, że nawet wyodrębnienie zmiennej do zasięgu wyżej nie wystarczy :)
Hej Xion,
szybkie info/fix:
Obecnie mamy już (dopiero? ;-)) “try with resources”,, vide: http://download.java.net/jdk7/docs/technotes/guides/language/try-with-resources.html
Kilka sub-standartów JDK7 jeszcze podlega głosowaniu, ale ta część akurat już sobie sprawnie działa i jest do pobrania.
Jakby ludzie robili przykłady jak trzeba, to nie byłoby dyskusji.
@ktoso: Szczęśliwi ci, którzy mogą używać Javy 7. Niestety wielu (np. programiści Androida) nie ma jeszcze tego luksusu :)
google guava pozwala w bardziej estetyczny sposób zamykać strumienie:
Closeables.closeQuietly(is);
Konstrukcja try-with-resources w Javie 7 zaproponowana swego czasu jako cześć Projektu Coin to sporo więcej niż tylko lukier składniowy – zapobiega między innymi bardzo niekorzystnemu zjawisku maskowania wyjątków
http://www.oracle.com/technetwork/articles/java/trywithresources-401775.html