Найти в Дзене

Как эффективно прочитать большой файл с помощью Java

Оглавление

1. Обзор

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

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. Потоковая передача через файл

Теперь давайте рассмотрим различные способы чтения данного файла по частям.

3.1. Использование сканера

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

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

3.2. Использование BufferedReader

Другим решением было бы использование класса BufferedReader.

Как правило,
этот класс предлагает удобный способ буферизации символов для упрощения процесса чтения файлов.

Для этой цели он предоставляет метод readLine(), который считывает содержимое данного файла построчно.

Итак, давайте посмотрим в действии:

try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
while (br.readLine() != null) {
// do something with each line
}
}

BufferedReader сокращает количество операций ввода-вывода, считывая файл фрагмент за фрагментом и кэшируя фрагменты во внутреннем буфере.

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

3.3. Использование Files.newBufferedReader()

В качестве альтернативы, мы можем использовать метод Files.newBufferedReader() для достижения того же результата:

try (BufferedReader br = java.nio.file.Files.newBufferedReader(Paths.get(fileName))) {
while (br.readLine() != null) {
// do something with each line
}
}

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

3.4. Использование SeekableByteChannel

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

Итак, давайте посмотрим на это на практике:

try (SeekableByteChannel ch = java.nio.file.Files.newByteChannel(Paths.get(fileName), StandardOpenOption.READ)) {
ByteBuffer bf = ByteBuffer.allocate(1000);
while (ch.read(bf) > 0) {
bf.flip();
// System.out.println(new String(bf.array()));
bf.clear();
}
}

Как показано выше, этот интерфейс поставляется с методом read(), который считывает последовательность байтов в буфер, обозначаемый ByteBuffer.

Как правило, метод flip() снова подготавливает буфер к записи. С другой стороны, clear(), как следует из названия, сбрасывает и очищает буфер.

Единственным недостатком этого подхода является то, что
нам нужно явно указывать размер буфера, используя метод allocate().

3.5. Использование Stream API

Аналогично, мы можем использовать Stream API для чтения и обработки содержимого файла.

Здесь мы будем использовать класс Files, который предоставляет метод lines() для возврата потока строковых элементов:

try (Stream<String> lines = java.nio.file.Files.lines(Paths.get(fileName))) {
lines.forEach(line -> {
// do something with each line
});
}

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

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

Того же самого можно достичь и с помощью библиотеки ввода-вывода Commons, используя пользовательский 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. Заключение

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

Оригинал статьи: https://www.baeldung.com/java-read-lines-large-file