1. Внешние ресурсы

Иногда в процессе работы Java-программа взаимодействует с объектами вне Java-машины. Например, с файлами на диске. Такие объекты принято называть внешними ресурсами. Внутренние ресурсы — это объекты, созданные внутри Java-машины.

Обычно взаимодействие происходит по такой схеме:

Оператор try with resources

Учет ресурсов

Операционная система ведет строгий учет доступных ресурсов, а также контролирует совместный доступ разных программ к ним. Например, если одна программа меняет какой-то файл, другая программа не может изменить (или удалить) этот файл. Это касается не только файлов, но на их примере понятнее всего.

У операционной системы есть функции (API), которые позволяют программе захватить какой-либо ресурс и/или освободить его. Если ресурс занят, с ним может работать только та программа, которая его захватила. Если ресурс свободен, любая программа может захватить его.

Представьте, что у вас в офисе есть общие кружки. Если кто-то взял кружку, другой уже не может взять ее. Но если ей попользовались, помыли и поставили на место, ее снова может брать кто угодно. Ну или места в метро или в маршрутке. Если место свободно — любой может его занять. Если место занято — им распоряжается тот, кто занял.

Захват внешних ресурсов

Каждый раз, когда ваша Java-программа начинает работать с каким-то файлом на диске, Java-машина запрашивает у операционной системы монопольный доступ к нему. Если ресурс свободен, его захватывает Java-машина.

Но после того, как вы закончили работать с файлом, этот ресурс (файл) нужно освободить: уведомить операционную систему, что он вам больше не нужен. Если вы этого не сделаете, ресурс будет продолжать числиться за вашей программой.

Для каждой запущенной программы операционная система ведет список занятых ресурсов. Если ваша программа превысит разрешенный ей лимит ресурсов, новые ресурсы операционная система вам уже не даст.

Хорошая новость в том, что если ваша программа завершилась, все ресурсы автоматически освобождаются (это делает сама операционная система).

Плохая же новость в том, что если вы пишете серверное приложение (а очень много серверных приложений пишутся на Java), ваш сервер должен работать днями, неделями, месяцами без остановки. И если вы в день открываете 100 файлов и не закрываете их, через пару недель ваше приложение исчерпает свой лимит и упадет. Не очень-то похоже на месяцы стабильной работы.


2. Метод close()

У классов, которые используют внешние ресурсы, есть специальный метод для их освобождения — close().

Ниже приведем пример программы, которая что-то пишет в файл и закрывает его за собой – освобождает ресурсы операционной системы. Выглядит это примерно так:

Код Примечание
String path = "c:\\projects\\log.txt";
FileOutputStream output = new FileOutputStream(path);
output.write(1);
output.close();
Путь к файлу.
Получаем объект файла: захватываем ресурс.
Пишем в файл
Закрываем файл — освобождаем ресурс

После работы с файлом (или другим внешним ресурсом) вы должны вызвать у объекта, связанного с внешним ресурсом, метод close().

Исключения

Вроде все просто. Однако в процессе работы программы могут возникнуть исключения, и внешний ресурс так и не будет освобожден. А это очень плохо.

Чтобы метод close() вызывался всегда, нужно обернуть наш код в блок try-catch-finally и добавить метод close() в блок finally. Выглядеть это будет примерно так:

try
{
   FileOutputStream output = new FileOutputStream(path);
   output.write(1);
   output.close();
}
catch (IOException e)
{
   e.printStackTrace();
}
finally
{
   output.close();
}

Этот код не скомпилируется, т.к. переменная output объявлена внутри блока try {}, а значит, не видна в блоке finally.

Исправляем:

FileOutputStream output = new FileOutputStream(path);

try
{
   output.write(1);
   output.close();
}
catch (IOException e)
{
   e.printStackTrace();
}
finally
{
   output.close();
}

Хорошо, но не будет работать, если ошибка возникла при создании объекта FileOutputStream, а это может произойти очень легко.

Исправляем:

FileOutputStream output = null;

try
{
   output = new FileOutputStream(path);
   output.write(1);
   output.close();
}
catch (IOException e)
{
   e.printStackTrace();
}
finally
{
   output.close();
}

Все еще есть несколько замечаний. Во-первых, если во время создания объекта FileOutputStream возникнет ошибка, переменная output будет null, и этот факт нужно учесть в блоке finally.

Во-вторых, метод close() вызывается в блоке finally всегда, а значит, он не нужен в блоке try. Финальный код будет выглядеть так:

FileOutputStream output = null;

try
{
   output = new FileOutputStream(path);
   output.write(1);
}
catch (IOException e)
{
   e.printStackTrace();
}
finally
{
   if (output != null)
      output.close();
}

Даже если не считать блок catch, который можно опустить, наш код из трех строк превратился в 10. Хотя по сути мы только открыли файл и записали в него 1. Немного громоздко, не находите ли?


3. try-with-resources

Создатели Java и тут решили нам подсыпать немного синтаксического сахарку. Начиная с 7-й версии Java, в ней появился новый оператор try-with-resources (try с ресурсами).

Он создан как раз для того, чтобы решать проблему с обязательным вызовом метода close(). В общем случае выглядит он довольно просто:

try (Класс имя = new Класс())
{
     Код, который работает с переменной имя
}

Это еще одна разновидность оператора try. После ключевого слова try нужно добавить круглые скобки, а внутри них — создать объекты с внешними ресурсами. Для объекта, указанного в круглых скобках, компилятор сам добавит секцию finally и вызов метода close().

Ниже написано два эквивалентных примера:

Длинный код Код с try-with-resources
FileOutputStream output = null;

try
{
   output = new FileOutputStream(path);
   output.write(1);
}
finally
{
   if (output != null)
   output.close();
}
try(FileOutputStream output = new FileOutputStream(path))
{
   output.write(1);
}

Код с использованием try-with-resources значительно короче и легче читается. А чем меньше кода, тем меньше шансов сделать опечатку или ошибку.

Кстати, у оператора try-with-resources можно дописывать блоки catch и finally. А можно и не добавлять, если в них нет необходимости.



4. Несколько переменных одновременно

Кстати, часто может возникнуть ситуация, когда нужно открыть несколько файлов одновременно. Допустим, вы копируете файл, и вам нужны два объекта: файл, из которого вы копируете данные и файл, в который вы копируете данные.

На этот случай оператор try-with-resources разрешает создавать в нем не один объект, а несколько. Код создания объектов должен разделяться точкой с запятой. Общий вид такой команды:

try (Класс имя = new Класс(); Класс2 имя2 = new Класс2())
{
   Код, который работает с переменной имя и имя2
}

Пример копирования файлов:

Длинный код Короткий код
String src = "c:\\projects\\log.txt";
String dest = "c:\\projects\\copy.txt";

FileInputStream input = null;
FileOutputStream output = null;

try
{
   input = new FileInputStream(src);
   output = new FileOutputStream(dest);

   byte[] buffer = input.readAllBytes();
   output.write(buffer);
}
finally
{
   if (input != null)
      input.close();
   if (output != null)
      output.close();
}
String src = "c:\\projects\\log.txt";
String dest = "c:\\projects\\copy.txt";

try(FileInputStream input = new FileInputStream(src);

FileOutputStream output = new FileOutputStream(dest))
{
   byte[] buffer = input.readAllBytes();
   output.write(buffer);
}

В общем, что тут скажешь: отличная это вещь — try-with-resources


5. Интерфейс AutoCloseable

Но и это еще не все. Внимательный читатель сразу начнет искать подвох границы применимости данного оператора.

А как будет работать оператор try-with-resources, если у класса нет метода close()? Ну, допустим, тогда ничего не вызовется. Нет метода, нет проблем.

А как будет работать try-with-resources если у класса есть несколько методов close()? И им нужно передавать параметры? И у класса нет метода close() без параметров?

Надеюсь, вы действительно задали себе эти вопросы, и возможно не только их.

Для того чтобы таких вопросов не было, создатели Java придумали специальный класс (интерфейс) AutoCloseable у которого только один метод – close() без параметров.

А также добавили ограничение, что в качестве ресурсов в try-with-resources можно передавать только объекты классов унаследованных от AutoCloseable. Таким образом у этих объектов всегда будет метод close() без параметров.

Кстати, как вы думаете, можно ли в качестве ресурса передать в try-with-resources объект, чей класс имеет метод close() без параметров, но который не унаследован от AutoCloseable?

Плохая новость: правильный ответ – нет, классы обязательно должны реализовывать интерфейс AutoCloseable.

Хорошая новость: в Java очень много классов реализовывают этот интерфейс, так что с большой долей вероятности все будет работать как надо.