1. Ієрархія винятків введення‑виведення
У Java під час роботи з файлами й іншими зовнішніми ресурсами майже завжди доводиться мати справу з так званими винятками введення‑виведення. Це об’єкти, які викидаються (throw) у разі, якщо щось пішло не так під час читання або запису даних. Наприклад, якщо файл не знайдено, немає доступу або диск раптово «втомився».
Головні герої нашої лекції
У Java є ціла ієрархія таких винятків. Ось основні з них:
- IOException — базовий клас для всіх помилок введення‑виведення. Якщо щось пішло не так із файлами, потоками чи мережею — майже завжди «винен» він (або хтось із його численних нащадків).
- FileNotFoundException — «нащадок» IOException, який з’являється, якщо ви намагаєтеся відкрити файл, якого не існує, або шлях указано неправильно.
Інші похідні:
- EOFException — теж прямий «нащадок» IOException. Сигналізує про те, що під час читання ми неочікувано дійшли до кінця файлу.
- MalformedInputException — «онук»: наслідує CharacterCodingException, який, своєю чергою, наслідує IOException. Така помилка виникає, якщо файл не можна коректно інтерпретувати в указаному кодуванні (наприклад, очікували "UTF-8", а надійшла бита послідовність).
- А ще є SocketException, ZipException та інші спеціалізовані «родичі», кожен зі своєю зоною відповідальності. Чим глибше в ієрархію, тим вужча й специфічніша ситуація.
Невелика спрощена схема:
java.lang.Exception
└── java.io.IOException
├── java.io.FileNotFoundException
├── java.io.EOFException
├── java.nio.charset.MalformedInputException
└── ... (інші)
Цікавий факт:
У Java майже всі операції з файлами потребують оголошувати або обробляти IOException — це так звані checked exceptions. Компілятор не дасть вам забути про обробку помилок!
2. Коли і чому виникають ці винятки
Відкриття неіснуючого файлу
Найчастіший випадок — ви намагаєтеся відкрити файл, а його немає. Це як прийти на зупинку, а автобуса не існує навіть у розкладі.
FileInputStream fis = new FileInputStream("abracadabra.txt"); // Бац! FileNotFoundException
Спроба запису у файл без прав
Якщо ви намагаєтеся записати файл у папку, де у вас немає прав, отримаєте FileNotFoundException або IOException (залежно від ситуації).
FileOutputStream fos = new FileOutputStream("/system/secret.txt"); // Бац! FileNotFoundException або IOException
Помилки читання і запису через пошкодження носія
Іноді файл начебто є, але диск пошкоджено, файл зайнятий іншою програмою або раптово вимкнули світло — тоді ви отримаєте IOException із різними повідомленнями.
Інші причини
- Файл відкрито лише для читання, а ви намагаєтеся записувати.
- Шлях до файлу надто довгий або містить неприпустимі символи.
- Файл використовується іншим процесом.
- Диск переповнений.
- Файл видалено іншим процесом між перевіркою та використанням.
3. Обробка за допомогою try-catch
Щоразу, коли ви працюєте з файлами, потоками, мережею — використовуйте try-catch. Це як подушка безпеки: якщо щось пішло не так, програма не впаде, а зможе коректно відреагувати.
Як правильно перехоплювати винятки?
У Java перехоплювати конкретніші винятки потрібно раніше, ніж загальні. Якщо поставити загальний catch (IOException e) першим, то вужчі, наприклад FileNotFoundException, просто не спрацюють — до них керування не дійде.
Правильна структура:
try {
// Робота з файлом
} catch (FileNotFoundException e) {
// Обробка ситуації "файл не знайдено"
} catch (IOException e) {
// Обробка інших помилок введення-виведення
}
Чому так?
Тому що FileNotFoundException — це окремий випадок IOException. Якщо ви перехопите загальний випадок раніше, то окремий «не дійде» до свого catch.
Приклад коду: обробка помилок під час відкриття файлу
import java.io.*;
public class FileReaderExample {
public static void main(String[] args) {
String filename = "notes.txt";
try {
BufferedReader reader = new BufferedReader(new FileReader(filename));
String line = reader.readLine();
System.out.println("Перший рядок файлу: " + line);
reader.close();
} catch (FileNotFoundException e) {
System.out.println("Файл не знайдено: " + filename);
} catch (IOException e) {
System.out.println("Помилка під час читання файлу: " + e.getMessage());
}
}
}
Зверніть увагу:
Навіть якщо ви перевірили, що файл існує, завжди використовуйте try-catch — файл може зникнути будь-якої миті (наприклад, його видалить інший процес).
4. Практика: пишемо код із обробкою помилок
Створимо прості приклади та зробимо так, щоб вони коректно реагували на відсутність файлу та інші помилки.
Крок 1: Спробуємо відкрити неіснуючий файл
import java.io.*;
public class NotesApp {
public static void main(String[] args) {
String filename = "my_notes.txt";
try {
BufferedReader reader = new BufferedReader(new FileReader(filename));
String line;
System.out.println("Ваші нотатки:");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
} catch (FileNotFoundException e) {
System.out.println("Ой! Файл із нотатками не знайдено: " + filename);
System.out.println("Порада: створіть файл або перевірте назву.");
} catch (IOException e) {
System.out.println("Сталася помилка під час читання файлу: " + e.getMessage());
}
}
}
Крок 2: Додамо обробку для запису у файл
import java.io.*;
public class NotesWriter {
public static void main(String[] args) {
String filename = "my_notes.txt";
try {
BufferedWriter writer = new BufferedWriter(new FileWriter(filename, true)); // append: true
writer.write("Нова нотатка!\n");
writer.close();
System.out.println("Нотатку успішно додано!");
} catch (IOException e) {
System.out.println("Помилка під час запису у файл: " + e.getMessage());
}
}
}
Крок 3: Універсальний приклад із логуванням помилок
import java.io.*;
public class SafeFileCopier {
public static void main(String[] args) {
String source = "source.txt";
String target = "target.txt";
try {
BufferedReader reader = new BufferedReader(new FileReader(source));
BufferedWriter writer = new BufferedWriter(new FileWriter(target));
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
reader.close();
writer.close();
System.out.println("Файл скопійовано успішно!");
} catch (FileNotFoundException e) {
System.out.println("Файл не знайдено: " + e.getMessage());
} catch (IOException e) {
System.out.println("Помилка введення-виведення: " + e.getMessage());
}
}
}
Порада:
У реальних застосунках варто логувати помилки не лише на екран, а й у файл або журнал, щоб згодом можна було розібратися, що саме пішло не так.
5. Таблиця: основні IO‑винятки
| Виняток | Коли виникає? | Як обробити? |
|---|---|---|
|
Файл не знайдено, шлях не існує, немає прав | Повідомити користувача, перевірити шлях/права, за потреби створити файл |
|
Неочікуваний кінець файлу під час читання | Повідомити про пошкодження/неповноту, спробувати частково відновити дані |
|
Загальна помилка введення‑виведення (диск, права, блокування) | Перевірити деталі, коректно завершити операцію, за можливості повторити спробу |
|
Некоректне кодування або структура файлу | Повідомити про пошкодження, спробувати інше кодування/джерело |
6. Типові помилки під час обробки IO‑винятків
Помилка № 1: Перехоплювати лише загальний Exception. Дуже спокусливо написати просто catch (Exception e), але тоді ви не зможете розрізнити, що саме пішло не так. Краще спочатку перехоплювати конкретні винятки (FileNotFoundException), а потім — загальний IOException.
Помилка № 2: Не закривати потоки у разі помилок. Якщо ви відкрили файл, а потім виник виняток, потік може залишитися незакритим. Використовуйте try-with-resources або закривайте ресурси у finally.
Помилка № 3: Ігнорувати повідомлення винятків. Не просто виводьте «Помилка!», а показуйте подробиці: e.getMessage(). Це допоможе швидше зрозуміти, що пішло не так.
Помилка № 4: Не обробляти FileNotFoundException під час запису. Багато хто думає, що FileNotFoundException — це лише про читання. Насправді він може виникати і під час запису (некоректний шлях, немає прав на створення файлу тощо).
Помилка № 5: Не перевіряти права доступу. Якщо програму запущено з обмеженими правами, багато операцій із файлами можуть завершитися помилкою. Завжди враховуйте це й інформуйте користувача.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ