java.io.Closeable
. Итак, сразу к делу.
Будем рассматривать на примере OutputStream
. Задача: получить на вход OutputStream
, сделать некоторую полезную работу с ним, закрыть OutputStream
.
Неправильное решение №1
OutputStream stream = openOutputStream(); // что-то делаем со stream stream.close();
Данное решение опасно, потому что если в коде сгенерируется исключение, то stream.close() не будет вызван. Произойдет утечка ресурса (не закроется соединение, не будет освобожден файловый дескриптор и т.д.)
Неправильное решение №2
Попробуем исправить предыдущий код. Используем try-finally
:
OutputStream stream = openOutputStream(); try { // что-то делаем со stream } finally { stream.close(); }
Теперь close()
всегда будет вызываться (ибо finally
): ресурс в любом случае будет освобождён. Вроде всё правильно. Ведь так?
Нет.
Проблема следующая. Метод close()
может сгенерировать исключение. И если при этом основной код работы с ресурсом тоже выбросит исключение, то оно перезатрется исключением из close()
. Информация об исходной ошибке пропадёт: мы никогда не узнаем, что было причиной исходного исключения.
Неправильное решение №3
Попробуем исправить ситуацию. Если stream.close()
может затереть «главное» исключение, то давайте просто «проглотим» исключение из close()
:
OutputStream stream = openOutputStream(); try { // что-то делаем со stream } finally { try { stream.close(); } catch (Throwable unused) { // игнорируем } }
Теперь вроде всё хорошо. Можем идти пить чай.
Как бы не так. Это решение ещё хуже предыдущего. Почему?
Потому что мы просто взяли и проглотили исключение из close()
. Допустим, что outputStream
— это FileOutputStream
, обёрнутый в BufferedOutputStream
. Так как BufferedOutputStream
делает flush()
на низлежащий поток порциями, то есть вероятность, что он его вызовет во время вызова close()
. Теперь представим, что файл, в который мы пишем, заблокирован. Тогда метод close()
выбросит IOException
, которое будет успешно «съедено». Ни одного байта пользовательских данных не записались в файл, и мы ничего об этом не узнали. Информация утеряна.
Если сравнить это решение с предыдущим, то там мы хотя бы узнаем, что произошло что-то плохое. Здесь же вся информация об ошибке пропадает.
Замечание: если вместо OutputStream
используется InputStream
, то такой код имеет право на жизнь. Дело в том, что если в InputStream.close()
выбрасывается исключение, то (скорее всего) никаких плохих последствий не будет, так как мы уже считали с этого потока всё что хотели. Это означает, что InputStream
и OutputStream
имеют совершенно разную семантику.
Неидеальное решение
Итак, как же всё-таки правильно выглядит код обработки ресурса?
Нам нужно учесть, что если основной код выбросит исключение, то это исключение должно иметь приоритет выше, чем то, которое может быть выброшено методом close()
. Это выглядит так:
OutputStream stream = openOutputStream(); Throwable mainThrowable = null; try { // что-то делаем со stream } catch (Throwable t) { // сохраняем исключение mainThrowable = t; // и тут же выбрасываем его throw t; } finally { if (mainThrowable == null) { // основного исключения не было. Просто вызываем close() stream.close(); } else { try { stream.close(); } catch (Throwable unused) { // игнорируем, так как есть основное исключение // можно добавить лог исключения (по желанию) } } }
Минусы такого решения очевидны: громоздко и сложно. Кроме того, пропадает информация об исключении из close()
, если основной код выбрасывает исключение. Также openOutputStream()
может вернуть null
, и тогда вылетит NullPointerException
(решается добавлением еще одного if’а, что приводит к ещё более громоздкому коду). Наконец, если у нас будет два ресурса (например, InputStream
и OutputStream
) и более, то код просто будет невыносимо сложным.
Правильное решение (Java 7)
В Java 7 появилась конструкция try-with-resources
. Используем её:
try (OutputStream stream = openOutputStream()) { // что-то делаем со stream }
И всё.
Если исключение будет выброшено в основном коде и в методе close()
, то приоритетнее будет первое исключение, а второе исключение будет подавлено, но информация о нем сохранится (с помощью метода Throwable.addSuppressed(Throwable exception)
, который вызывается неявно Java компилятором):
Exception in thread "main" java.lang.RuntimeException: Main exception at A$1.write(A.java:16) at A.doSomething(A.java:27) at A.main(A.java:8) Suppressed: java.lang.RuntimeException: Exception on close() at A$1.close(A.java:21) at A.main(A.java:9)
Правильное решение (Java 6 с использованием Google Guava)
В Java 6 средствами одной лишь стандартной библиотеки не обойтись. Однако нам на помощь приходит замечательная библиотека Google Guava. В Guava 14.0 появился класс com.google.common.io.Closer
(try-with-resources
для бедных), с помощью которого неидеальное решение выше можно заметно упростить:
Closer closer = Closer.create(); try { OutputStream stream = closer.register(openOutputStream()); // что-то делаем со stream } catch (Throwable e) { // ловим абсолютно все исключения (и даже Error'ы) throw closer.rethrow(e); } finally { closer.close(); }
Решение заметно длиннее, чем в случае Java 7, но всё же намного короче неидеального решения. Вывод будет примерно таким же, как Java 7.
Closer
также поддерживает произвольное количество ресурсов в нём (метод register(...)
). К сожалению, Closer
— это класс, помеченный аннотацией @Beta
, а значит может подвергнуться значительным изменениям в будущих версиях библиотеки (вплоть до удаления).
Выводы
Правильно освобождать ресурсы не так просто, как кажется (просто только в Java 7). Всегда уделяйте этому должное внимание. InputStream
и OutputStream
(Reader
и Writer
) обрабатываются по-разному (по крайней мере в Java 6)!
Дополнения/исправления приветствуются!
В следующий раз я планирую рассказать, как бороться с NullPointerException
.
ссылка на оригинал статьи http://habrahabr.ru/post/178405/
Добавить комментарий