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: Не проверять права доступа. Если программа запускается с ограниченными правами, многие операции с файлами могут завершиться ошибкой. Всегда учитывайте это и информируйте пользователя.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ