«1. Обзор

Этот туториал покажет, как эффективно прочитать все строки из большого файла в Java.

Эта статья является частью учебника «Java — Back to Basic» здесь, на Baeldung.

2. Чтение в памяти

Стандартный способ чтения строк файла — в памяти — и Guava, и Apache Commons IO предоставляют быстрый способ сделать именно это:

Files.readLines(new File(path), Charsets.UTF_8);
FileUtils.readLines(new File(path));

~~ ~ Проблема с этим подходом заключается в том, что все строки файла хранятся в памяти, что быстро приведет к ошибке OutOfMemoryError, если файл достаточно велик.

Например, чтение файла ~1 Гб:

@Test
public void givenUsingGuava_whenIteratingAFile_thenWorks() throws IOException {
    String path = ...
    Files.readLines(new File(path), Charsets.UTF_8);
}

Это начинается с потребления небольшого объема памяти: (потреблено ~0 Мб)

[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Total Memory: 128 Mb
[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Free Memory: 116 Mb

Однако после полного файла была обработана, в итоге имеем: (~2 Гб израсходовано)

[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Total Memory: 2666 Mb
[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Free Memory: 490 Mb

Значит, около 2,1 Гб памяти потребляется процессом — причина проста — строки файла теперь все хранится в памяти.

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

Более того, нам обычно не нужно, чтобы все строки файла находились в памяти сразу — вместо этого нам просто нужно иметь возможность перебирать каждую из них, выполнять некоторую обработку и отбрасывать ее. Итак, это именно то, что мы собираемся делать — перебирать строки, не удерживая их все в памяти.

3. Потоковая передача через файл

Давайте теперь рассмотрим решение – мы собираемся использовать java.util.Scanner для последовательного просмотра содержимого файла и извлечения строк, одна за другой:

FileInputStream inputStream = null;
Scanner sc = null;
try {
    inputStream = new FileInputStream(path);
    sc = new Scanner(inputStream, "UTF-8");
    while (sc.hasNextLine()) {
        String line = sc.nextLine();
        // System.out.println(line);
    }
    // note that Scanner suppresses exceptions
    if (sc.ioException() != null) {
        throw sc.ioException();
    }
} finally {
    if (inputStream != null) {
        inputStream.close();
    }
    if (sc != null) {
        sc.close();
    }
}

Это решение будет перебирать все строки в файле, позволяя обрабатывать каждую строку, не сохраняя ссылки на них, и, наконец, не сохраняя их в памяти: (~150 МБ потребляется)

[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Total Memory: 763 Mb
[main] INFO  org.baeldung.java.CoreJavaIoUnitTest - Free Memory: 605 Mb

4. Потоковая передача с помощью Apache Commons IO

То же самое может быть достигнуто с использованием библиотеки Commons IO, с помощью пользовательского LineIterator, предоставленного библиотекой:

LineIterator it = FileUtils.lineIterator(theFile, "UTF-8");
try {
    while (it.hasNext()) {
        String line = it.nextLine();
        // do something with line
    }
} finally {
    LineIterator.closeQuietly(it);
}

Поскольку весь файл находится не полностью в памяти — это также приведет к довольно консервативным значениям потребления памяти: (потреблено ~150 Мб)

[main] INFO  o.b.java.CoreJavaIoIntegrationTest - Total Memory: 752 Mb
[main] INFO  o.b.java.CoreJavaIoIntegrationTest - Free Memory: 564 Mb

5. Заключение

Эта короткая статья показывает, как обрабатывать строки в большом файл без итерации, без исчерпания доступной памяти, что оказывается весьма полезным при работе с такими большими файлами.

Реализацию всех этих примеров и фрагментов кода можно найти в нашем проекте GitHub — это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.