Как прочитать файл в строку Java: разбор под капотом

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как прочитать файл в строку Java: разбор популярного вопроса с Stack Overflow

Если вы когда-либо задавались вопросом «How do I create a Java string from the contents of a file?» — вы не одиноки. Этот вопрос набрал тысячи просмотров на Stack Overflow, и неспроста. Казалось бы, банальная операция — прочитать текстовый файл и поместить его содержимое в строку — таит в себе множество подводных камней: от утечек памяти до проблем с кодировкой.

В этой статье мы не просто дадим готовые рецепты. Мы заглянем под капот каждого метода: как работает BufferedReader, почему Files.readString() появился только в Java 11, и чем Scanner отличается от InputStream. Поехали!

Почему это сложнее, чем кажется?

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

  • Открыть поток данных (InputStream, Reader)
  • Буферизировать чтение (чтобы не дёргать диск на каждый байт)
  • Собрать байты/символы в единый буфер
  • Преобразовать в строку с учётом кодировки
  • Закрыть ресурсы (чтобы не было утечки файловых дескрипторов)

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

Способ 1: Классика — BufferedReader (Java 1.1+)

Самый старый и проверенный метод. Использует BufferedReader для построчного чтения. Под капотом буфер (по умолчанию 8 КБ) накапливает данные, снижая количество системных вызовов read().

public static String readFileBufferedReader(String filePath) throws IOException {
    StringBuilder content = new StringBuilder();
    try (BufferedReader reader = new BufferedReader(new FileReader(filePath, StandardCharsets.UTF_8))) {
        String line;
        while ((line = reader.readLine()) != null) {
            content.append(line).append(System.lineSeparator());
        }
    }
    return content.toString();
}

Как это работает под капотом?

FileReader открывает файл и создаёт FileInputStream. BufferedReader оборачивает его, добавляя буфер. При вызове readLine() читается блок данных (например, 8 КБ), и строка собирается из буфера. Если строка длиннее буфера, буфер перечитывается. try-with-resources гарантирует закрытие потока даже при исключении.

Плюсы: Работает на любой версии Java, контролируемое построчное чтение.
Минусы: Добавляет лишний разделитель строк в конец, неэффективен для огромных файлов (строка в памяти).

Способ 2: Scanner — удобно, но медленно (Java 5+)

Многие любят Scanner за простоту. Он умеет разбивать ввод на токены, но для чтения целого файла используется так:

public static String readFileScanner(String filePath) throws IOException {
    try (Scanner scanner = new Scanner(new File(filePath), StandardCharsets.UTF_8.name())) {
        scanner.useDelimiter("\\A"); // Начало файла как разделитель
        return scanner.hasNext() ? scanner.next() : "";
    }
}

Магия \\A: Это регулярное выражение, обозначающее начало входных данных. Устанавливая его как разделитель, мы заставляем Scanner прочитать всё содержимое за один раз. Под капотом Scanner использует Pattern для поиска разделителя, что добавляет накладные расходы на регулярные выражения.

Плюсы: Очень лаконичный код.
Минусы: Медленнее BufferedReader из-за работы с регулярками, не подходит для бинарных файлов.

Способ 3: Files.readAllLines() — удобство Java 8 (NIO)

Начиная с Java 8, пакет java.nio.file предложил элегантное решение:

public static String readFileNIO(String filePath) throws IOException {
    List<String> lines = Files.readAllLines(Paths.get(filePath), StandardCharsets.UTF_8);
    return String.join(System.lineSeparator(), lines);
}

Под капотом: Files.readAllLines() внутри использует BufferedReader и читает все строки в ArrayList. Затем String.join() склеивает их. Это удобно, но создаёт промежуточный список строк — лишнее потребление памяти.

Плюсы: Чистый код, встроенная поддержка кодировки.
Минусы: Двойная память (список + строка), не для гигантских файлов.

Способ 4: Files.readString() — современный стандарт (Java 11+)

Java 11 наконец-то добавила то, о чём просили годы — однострочное чтение файла в строку:

public static String readFileModern(String filePath) throws IOException {
    return Files.readString(Paths.get(filePath));
}

Под капотом (самое интересное): Метод Files.readString() внутри делает следующее:

  • Открывает FileChannel (канал для прямого доступа к файлу)
  • Создаёт InputStream из канала
  • Читает все байты в массив byte[] с помощью цикла с буфером (8 КБ)
  • Конвертирует массив байтов в строку через new String(bytes, charset), где charset по умолчанию — UTF-8

Этот метод оптимизирован: нет лишних обёрток, нет построчного чтения, минимальное количество аллокаций. Для файлов до 2 ГБ — идеальный выбор.

Плюсы: Максимально лаконично, оптимальная производительность, кодировка UTF-8 по умолчанию.
Минусы: Только Java 11+, весь файл в памяти.

Способ 5: InputStream и ByteArrayOutputStream — для пуристов

Иногда нужно читать бинарные данные или работать с произвольными потоками. Тогда пригодится классика:

public static String readFileStream(String filePath) throws IOException {
    try (InputStream is = new FileInputStream(filePath);
         ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
        byte[] buffer = new byte[8192];
        int bytesRead;
        while ((bytesRead = is.read(buffer)) != -1) {
            baos.write(buffer, 0, bytesRead);
        }
        return baos.toString(StandardCharsets.UTF_8.name());
    }
}

Под капотом: FileInputStream читает байты с диска. ByteArrayOutputStream динамически расширяет внутренний массив (как ArrayList для байтов). При каждом переполнении создаётся новый массив в 1.5 раза больше, данные копируются. Это неэффективно для больших файлов, но даёт полный контроль.

Тест производи

Блоги

Рекомендаций по книгам