1. Зовнішні ресурси

Часом у процесі роботи Java-програма взаємодіє з об'єктами поза Java-машиною, наприклад із файлами на диску. Такі об'єкти називають зовнішніми ресурсами. Внутрішні ресурси — це об'єкти, створені всередині Java-машини.

Зазвичай взаємодія відбувається за такою схемою:

Облік ресурсів

Операційна система веде суворий облік доступних ресурсів, а також контролює спільний доступ до них різних програм. Наприклад, якщо якась програма змінює певний файл, інша програма не може цей файл змінити (або видалити). Це стосується не тільки файлів, але цей приклад найзрозуміліший.

Операційна система має функції (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 й тут підсипали нам дещицю синтаксичного цукру. У мові Java починаючи з 7-ї версії з'явився новий оператор 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 дуже багато класів реалізовують цей інтерфейс, тож, найімовірніше, все працюватиме як слід.