☕ Java

Работа с файлами в Java: подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы пишете сочинение в тетради. Пока вы пишете — информация находится в вашей голове и на кончике ручки. Но как только вы закрыли тетрадь и положили её в рюкзак — информация сохранилась. Завтра вы откроете тетрадь снова и продолжите писать. То же самое происходит в программировании: пока программа работает, все данные живут в оперативной памяти (как в голове). Как только программа закрывается — память очищается. Чтобы данные остались навсегда, их нужно сохранить в файл (как в тетрадь).

Работа с файлами в Java — это умение программы читать информацию из файлов (например, открыть сохранённую игру) и записывать информацию в файлы (сохранить прогресс). Без этого любая программа была бы «беспамятной»: каждый запуск начинался бы с чистого листа.

💡 Важно: Файлы бывают разные: текстовые (.txt, .csv, .json), бинарные (картинки, музыка, архивы). В этой статье мы сосредоточимся на текстовых файлах — это основа основ.

Теория: как это работает изнутри

Чтобы прочитать файл, Java должна пройти несколько этапов:

  1. Открыть «дверь» к файлу — создать объект, который указывает на нужный файл на жёстком диске.
  2. Создать «трубу» для чтения — специальный объект-поток (stream), который умеет забирать данные из файла.
  3. Читать данные кусочками — либо по одному символу, либо строками, либо целыми массивами.
  4. Закрыть «трубу» — обязательно! Иначе файл останется заблокированным, и другие программы не смогут к нему обратиться.

В Java существует два основных подхода к чтению файлов:

Подход Суть Когда использовать
Байтовые потоки (InputStream) Читают файл как набор байтов (0 и 1) Для картинок, музыки, видео — любых нетекстовых данных
Символьные потоки (Reader) Читают файл как текст, учитывая кодировку (UTF-8, Windows-1251) Для текстовых файлов — документы, конфиги, логи

В этой статье мы будем работать с символьными потоками, так как они удобнее для чтения текста.

Главные классы для чтения текстовых файлов в Java:

Класс Назначение Особенность
FileReader Базовое чтение символов из файла Работает с кодировкой по умолчанию (может быть проблема с русским текстом)
BufferedReader Чтение с буферизацией (накоплением данных в памяти) В 10-100 раз быстрее FileReader, умеет читать целые строки
Scanner Удобное чтение с разбором на слова и числа Медленнее BufferedReader, но удобнее для маленьких файлов
Files.readAllLines() Чтение всего файла сразу в список строк Удобно, но не подходит для огромных файлов (съедает много памяти)

Синтаксис в Java: все формы и варианты

Давайте разберём три основных способа чтения файла. Каждый способ имеет свои плюсы и минусы.

Способ 1: Классический — FileReader + BufferedReader

Это «дедушкин» способ, который работает в Java с самой первой версии. Он надёжный, быстрый и понятный.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class ClassicRead {
    public static void main(String[] args) {
        // Создаём переменную для ридера вне try-блока
        BufferedReader reader = null;
        
        try {
            // Шаг 1: Открываем файл
            FileReader fileReader = new FileReader("input.txt");
            
            // Шаг 2: Оборачиваем в буферизированный ридер для скорости
            reader = new BufferedReader(fileReader);
            
            // Шаг 3: Читаем первую строку
            String line = reader.readLine();
            
            // Шаг 4: Пока строка не пустая (не null) — читаем дальше
            while (line != null) {
                System.out.println(line);  // Выводим строку на экран
                line = reader.readLine();  // Читаем следующую строку
            }
            
        } catch (IOException e) {
            // Если что-то пошло не так — ловим ошибку
            System.err.println("Ошибка при чтении файла: " + e.getMessage());
        } finally {
            // Шаг 5: ВАЖНО! Закрываем ридер в блоке finally
            // Это гарантирует закрытие даже если произошла ошибка
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (IOException e) {
                System.err.println("Ошибка при закрытии файла: " + e.getMessage());
            }
        }
    }
}
⚠️ Внимание: Блок finally выполняется всегда — и после успешного чтения, и после ошибки. Это гарантирует, что файл будет закрыт.

Способ 2: Современный — try-with-resources (Java 7+)

Начиная с Java 7, придумали более элегантный способ. Ресурс (файл) автоматически закрывается, когда мы выходим из try-блока. Кода становится меньше, а надёжность — выше.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TryWithResourcesRead {
    public static void main(String[] args) {
        // Ресурс объявляется в круглых скобках после try
        // Java сама закроет его при выходе из блока
        try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
            
            String line;
            // Читаем строки, пока они не закончатся
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
            
        } catch (IOException e) {
            System.err.println("Ошибка: " + e.getMessage());
        }
        // Закрывать вручную не нужно — Java сделает это автоматически!
    }
}
✅ Совет: Всегда используйте try-with-resources. Это современный стандарт, который делает код чище и безопаснее.

Способ 3: Ультрасовременный — Files и Path (Java 8+)

В Java 8 добавили класс Files с кучей удобных методов. Можно прочитать весь файл одной командой!

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.List;

public class FilesReadAllLines {
    public static void main(String[] args) {
        try {
            // Создаём путь к файлу
            Path path = Paths.get("input.txt");
            
            // Читаем ВСЕ строки файла в список (List)
            List<String> allLines = Files.readAllLines(path);
            
            // Выводим все строки на экран
            for (String line : allLines) {
                System.out.println(line);
            }
            
        } catch (IOException e) {
            System.err.println("Ошибка: " + e.getMessage());
        }
    }
}
⚠️ Предупреждение: Files.readAllLines() загружает весь файл в память. Если файл весит 10 ГБ — программа упадёт с ошибкой OutOfMemoryError. Для больших файлов используйте BufferedReader.

Способ 4: Чтение через Scanner

Scanner умеет не только читать строки, но и разбивать их на слова и числа. Удобно для парсинга простых данных.

import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class ScannerRead {
    public static void main(String[] args) {
        try (Scanner scanner = new Scanner(new File("input.txt"))) {
            
            // Читаем слово за словом (разделитель — пробел)
            while (scanner.hasNext()) {
                String word = scanner.next();
                System.out.println("Слово: " + word);
            }
            
        } catch (FileNotFoundException e) {
            System.err.println("Файл не найден: " + e.getMessage());
        }
    }
}

Примеры кода — от простого к сложному

Пример 1: Чтение одной строки

import java.io.*;
import java.nio.file.*;

public class ReadOneLine {
    public static void main(String[] args) throws IOException {
        // Самый простой способ прочитать первую строку файла
        Path path = Paths.get("greeting.txt");
        String firstLine = Files.readAllLines(path).get(0);
        System.out.println("Первая строка: " + firstLine);
    }
}

Пример 2: Подсчёт строк в файле

import java.io.*;

public class CountLines {
    public static void main(String[] args) {
        int lineCount = 0;
        
        try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
            while (reader.readLine() != null) {
                lineCount++;  // Считаем каждую прочитанную строку
            }
            System.out.println("Всего строк: " + lineCount);
        } catch (IOException e) {
            System.err.println("Ошибка: " + e.getMessage());
        }
    }
}

Пример 3: Поиск слова в файле

import java.io.*;

public class FindWord {
    public static void main(String[] args) {
        String searchWord = "Java";
        int lineNumber = 0;
        boolean found = false;
        
        try (BufferedReader reader = new BufferedReader(new FileReader("article.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                lineNumber++;
                if (line.contains(searchWord)) {
                    System.out.println("Слово '" + searchWord + "' найдено в строке " + lineNumber);
                    System.out.println("Содержимое строки: " + line);
                    found = true;
                }
            }
            if (!found) {
                System.out.println("Слово '" + searchWord + "' не найдено в файле.");
            }
        } catch (IOException e) {
            System.err.println("Ошибка: " + e.getMessage());
        }
    }
}

Пример 4: Чтение чисел из файла и вычисление суммы

import java.io.*;
import java.util.Scanner;

public class SumNumbers {
    public static void main(String[] args) {
        int sum = 0;
        
        try (Scanner scanner = new Scanner(new File("numbers.txt"))) {
            // Пока есть числа — читаем и суммируем
            while (scanner.hasNextInt()) {
                int number = scanner.nextInt();
                sum += number;
                System.out.println("Прочитано число: " + number);
            }
            System.out.println("Сумма всех чисел: " + sum);
        } catch (FileNotFoundException e) {
            System.err.println("Файл не найден!");
        }
    }
}

Пример 5: Чтение CSV-файла (таблица с разделителями)

import java.io.*;

public class ReadCSV {
    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new FileReader("students.csv"))) {
            String line;
            
            // Пропускаем заголовок (первую строку)
            String header = reader.readLine();
            System.out.println("Заголовок: " + header);
            
            // Читаем данные
            while ((line = reader.readLine()) != null) {
                // Разделяем строку по запятой
                String[] parts = line.split(",");
                
                // parts[0] — имя, parts[1] — возраст, parts[2] — оценка
                String name = parts[0];
                int age = Integer.parseInt(parts[1]);
                double grade = Double.parseDouble(parts[2]);
                
                System.out.println("Студент: " + name + ", возраст: " + age + ", оценка: " + grade);
            }
        } catch (IOException e) {
            System.err.println("Ошибка: " + e.getMessage());
        }
    }
}

Пример 6: Чтение файла с указанием кодировки (для русского текста)

import java.io.*;

public class ReadWithEncoding {
    public static void main(String[] args) {
        // Важно! Если файл сохранён в UTF-8, а мы читаем в Windows-1251 —
        // получим кракозябры. Указываем правильную кодировку.
        String encoding = "UTF-8";  // или "Windows-1251", "KOI8-R"
        
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(
                    new FileInputStream("russian.txt"), encoding))) {
            
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            System.err.println("Ошибка: " + e.getMessage());
        }
    }
}

Пример 7: Чтение большого файла частями (для экономии памяти)

import java.io.*;

public class ReadLargeFile {
    public static void main(String[] args) {
        final int BUFFER_SIZE = 8192;  // Размер буфера — 8 КБ
        
        try (BufferedReader reader = new BufferedReader(
                new FileReader("huge_file.txt"), BUFFER_SIZE)) {
            
            char[] buffer = new char[BUFFER_SIZE];
            int charsRead;
            
            // Читаем не строками, а блоками символов
            while ((charsRead = reader.read(buffer, 0, BUFFER_SIZE)) != -1) {
                // Превращаем прочитанные символы в строку
                String chunk = new String(buffer, 0, charsRead);
                System.out.print(chunk);  // Выводим без перевода строки
            }
        } catch (IOException e) {
            System.err.println("Ошибка: " + e.getMessage());
        }
    }
}

Сравнение с Python: чем похоже и чем отличается

Аспект Java Python
Открытие файла new FileReader("file.txt") или Files.newBufferedReader(path) open("file.txt", "r")
Чтение строки reader.readLine() — возвращает строку или null file.readline() — возвращает строку или пустую строку
Чтение всех строк Files.readAllLines(path) — возвращает List<String> file.readlines() — возвращает список строк
Итерация по строкам Цикл while с проверкой на null Цикл for line in file: — элегантно и просто
Закрытие файла try-with-resources или finally блок with open(...) as file: — автоматически
Обработка ошибок IOException — нужно ловить или пробрасывать Исключения тоже есть, но их можно не обрабатывать
Скорость Очень быстрый (компилируемый язык) Медленнее (интерпретируемый), но для файлов разница невелика
Кодировка Нужно указывать явно через InputStreamReader Можно указать в open(): encoding="utf-8"
💡 Вывод: Python проще для новичков — меньше кода, интуитивно понятно. Java требует больше усилий, но даёт больше контроля и скорости. Если вы знаете Python, вы легко освоите Java — концепции те же, разница только в синтаксисе.

Типичные ошибки и подводные камни

Ошибка Почему возникает Как исправить
FileNotFoundException Файл не существует, путь указан неверно, нет прав доступа Проверьте путь, используйте абсолютный путь или проверяйте существование файла через file.exists()
NullPointerException Попытка вызвать метод на null-объекте (например, reader = null) Всегда проверяйте reader на null перед использованием
IOException: Stream closed Попытка читать из уже закрытого потока Не закрывайте поток раньше времени, используйте try-with-resources
OutOfMemoryError Попытка прочитать слишком большой файл целиком Используйте BufferedReader, читайте построчно или блоками
Кракозябры вместо текста Несовпадение кодировки файла и кодировки чтения Узнайте кодировку файла и укажите её при чтении
Ресурс не закрыт Забыли закрыть файл в finally или не используете try-with-resources Всегда используйте try-with-resources — это безопасно
NumberFormatException Попытка преобразовать строку в число, но строка содержит буквы Проверяйте данные перед преобразованием, используйте try-catch
⚠️ Важное правило: Никогда не предполагайте, что файл существует или что он не пустой. Всегда проверяйте и обрабатывайте исключения.

Лучшие практики и советы профессионалов

  1. Всегда используйте try-with-resources — это современный стандарт Java. Он гарантирует закрытие файла даже при возникновении исключения.
  2. Указывайте кодировку явно — особенно если работаете с русским текстом. Лучше всего использовать UTF-8.
  3. Не читайте большие файлы целиком — используйте BufferedReader и читайте построчно. Это экономит память и ускоряет работу.
  4. Проверяйте существование файла — перед чтением вызовите file.exists() и file.isFile().
  5. Используйте относительные пути — если файл лежит в папке проекта. Абсолютные пути (C:Users...) делают программу непереносимой.
  6. Логируйте ошибки — вместо System.err.println используйте библиотеку логирования (Log4j, SLF4J) в серьёзных проектах.
  7. Не игнорируйте исключения — пустой catch блок — это зло. Всегда хотя бы выводите сообщение об ошибке.
  8. Используйте Path и Files — это современный API (Java 8+), который проще и безопаснее старых File и FileReader.
  9. Тестируйте с разными файлами — пустой файл, файл с одной строкой, файл с миллионом строк. Программа должна работать корректно во всех случаях.
  10. Закрывайте ресурсы в обратном порядке — если открыли несколько потоков, закрывайте их в порядке, обратном открытию.

Вопросы для самопроверки

  1. Вопрос: Что произойдёт, если не закрыть файл после чтения?
    Ответ: Файл останется заблокированным операционной системой. Другие программы (и даже ваша же программа при следующем запуске) не смогут к нему обратиться. Это называется «утечка ресурсов».
  2. Вопрос: В чём разница между FileReader и BufferedReader?
    Ответ: FileReader читает по одному символу за раз — это медленно. BufferedReader использует буфер (область памяти), накапливает данные и читает большими кусками — это в десятки раз быстрее.
  3. Вопрос: Какой способ чтения файла самый безопасный и современный?
    Ответ: try-with-resources с BufferedReader. Он автоматически закрывает файл, обрабатывает ошибки и работает быстро.
  4. Вопрос: Что такое кодировка и почему она важна?
    Ответ: Кодировка — это способ превращения символов в байты. Если файл сохранён в UTF-8, а вы читаете его как Windows-1251, русские буквы превратятся в кракозябры (Привет).
  5. Вопрос: Как прочитать файл, если он очень большой (10 ГБ)?
    Ответ: Используйте BufferedReader и читайте построчно. Никогда не используйте Files.readAllLines() — он загрузит весь файл в память и вызовет OutOfMemoryError.
  6. Вопрос: Что такое try-with-resources и как он работает?
    Ответ: Это конструкция try, в которой объявляются ресурсы (файлы, потоки). Java автоматически вызывает метод close() для всех ресурсов при выходе из блока — и при успехе, и при ошибке.
  7. Вопрос: Как узнать, существует ли файл перед чтением?
    Ответ: Используйте Files.exists(path) или new File("file.txt").exists(). Это вернёт true, если файл существует.
  8. Вопрос: В чём разница между относительным и абсолютным путём?
    Ответ: Абсолютный путь начинается с корня диска (C:Users...). Относительный путь указывается относительно текущей папки проекта (data/input.txt). Относительные пути лучше — программа будет работать на любом компьютере.
  9. Вопрос: Что такое IOException и почему его нужно обрабатывать?
    Ответ: IOException — это исключение, которое возникает при ошибках ввода-вывода (файл не найден, нет прав доступа, диск переполнен). Его нужно обрабатывать, чтобы программа не упала, а сообщила пользователю об ошибке.
  10. Вопрос: Можно ли читать файл и писать в него одновременно?
    Ответ: Можно, но это опасно. Если два потока (или две программы) одновременно пишут в файл, данные могут перемешаться. Для безопасной работы используйте блокировки (FileLock) или базы данных.

Итог и что изучить дальше

Мы разобрали, как читать текстовые файлы в Java. Вы узнали:

  • Зачем нужно читать файлы — чтобы данные сохранялись между запусками программы.
  • Как устроено чтение изнутри — открытие, чтение, закрытие.
  • Четыре способа чтения: классический, try-with-resources, Files API, Scanner.
  • Семь примеров — от простого чтения строки до обработки CSV и больших файлов.
  • Сравнение с Python — концепции похожи, синтаксис разный.
  • Типичные ошибки — и как их избежать.
  • Лучшие практики — чтобы писать профессиональный код.

Что изучать дальше:

  1. Запись в файлы — FileWriter, BufferedWriter, Files.write(). Умение читать без умения писать — половина дела.
  2. Работа с бинарными файлами — FileInputStream, BufferedInputStream. Для картинок, музыки, архивов.
  3. Формат JSON — библиотеки Jackson, Gson. Современные программы обмениваются данными в JSON.
  4. Формат XML — DOM, SAX парсеры. Всё ещё используется в старых проектах.
  5. Работа с файловой системой — создание папок, удаление файлов, перемещение, получение списка файлов.
  6. NIO.2 (New I/O) — современный API для работы с файлами, асинхронное чтение, каналы.
  7. Сериализация объектов — сохранение целых объектов Java в файл и восстановление их обратно.
🎯 Главный совет: Не бойтесь экспериментировать. Создайте текстовый файл, напишите программу, которая его читает, измените файл, прочитайте снова. Только практика превращает теорию в навык. Удачи в программировании!