— Смирно, ученик Амиго!

— Рад изучать Java, капитан!

— Вольно, Амиго. Сегодня у нас супер-интересная тема. Мы поговорим о взаимодействии Java-программы с внешними ресурсами и изучим один очень интересный оператор. Ушами лучше не хлопать.

— Я весь внимание.

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

— А что тогда считается внутренними ресурсами?

— Внутренние ресурсы — это объекты, созданные внутри Java-машины. Обычно взаимодействие происходит по такой схеме:

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

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

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

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

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

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

— Звучит справедливо.

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

— Это как программы, которые могут съесть всю память…

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

— Раз есть хорошая новость, то и плохая поджидает?

— Так точно. Плохая новость состоит в том, что если ты пишешь серверное приложение…

— А я пишу такие приложения?

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

— То есть программа не завершается, соответственно, память, не освобождается автоматически.

— Именно. И если ты в день открываешь 100 файлов и не закрываешь их, то через пару недель твоё приложение исчерпает свой лимит и упадет.

— Не очень-то похоже на месяцы стабильной работы! Что же делать?

— У классов, которые используют внешние ресурсы, есть специальный метод для их освобождения — 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.

— Ффух… Хорошо, что это всё закончилось. Относительно понятно, но как-то громоздко что ли?

— Так и есть. Поэтому создатели Java нам подсобили и подсыпали немного синтаксического сахарку. Переходим к гвоздю программы, а точнее — этой лекции:

try-with-resources

— Начиная с 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. А можно и не добавлять, если в них нет необходимости.

18
Задача
Модуль 1. Java Syntax,  24 уровень2 лекция
Недоступна
Зри в корень
Напиши программу, в которой пользователь вводит с клавиатуры путь к файлу или папке, после чего в консоли выводится диск (корень для Unix-like), на котором находится этот файл (или папка). Для решения задачи используй Path и его методы.
18
Задача
Модуль 1. Java Syntax,  24 уровень2 лекция
Недоступна
Все относительно
Напиши программу, которая будет считывать с клавиатуры два пути и выводить в консоль относительный путь между первым и вторым путями, если он существует. В противном случае выводить ничего не нужно.

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

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

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

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

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

Короткий код Длинный код
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);
}
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();
}

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

— Что тут скажешь: пользуйся.