Правильно освобождаем ресурсы в Java

в 18:40, , рубрики: guava, java, метки: , ,

Неправильное освобождение ресурсов — одна из наиболее часто допускаемых ошибок среди Java-программистов. Под ресурсом в данной статье я буду подразумевать всё, что реализует интерфейс 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.

Автор: orionll

Источник

Поделиться

* - обязательные к заполнению поля