1. Обзор
В этом руководстве показано, как эффективно прочитать все строки из большого файла на Java .
Эта статья является частью руководства « Java — Back to Basic
» здесь, на ForEach.
2. Чтение в памяти
Стандартный способ чтения строк файла находится в памяти — и Guava, и Apache Commons IO предоставляют быстрый способ сделать это:
Files.readLines(new File(path), Charsets.UTF_8);
FileUtils.readLines(new File(path));
Проблема с этим подходом заключается в том, что все строки файла хранятся в памяти, что быстро приведет к ошибке OutOfMemoryError
, если файл достаточно велик.
Например — чтение файла ~ 1Gb :
@Test
public void givenUsingGuava_whenIteratingAFile_thenWorks() throws IOException {
String path = ...
Files.readLines(new File(path), Charsets.UTF_8);
}
Это начинается с небольшого объема потребляемой памяти: (используется ~ 0 МБ)
[main] INFO org.foreach.java.CoreJavaIoUnitTest - Total Memory: 128 Mb
[main] INFO org.foreach.java.CoreJavaIoUnitTest - Free Memory: 116 Mb
Однако после обработки всего файла в конце мы имеем: (израсходовано ~2 Гб)
[main] INFO org.foreach.java.CoreJavaIoUnitTest - Total Memory: 2666 Mb
[main] INFO org.foreach.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.foreach.java.CoreJavaIoUnitTest - Total Memory: 763 Mb
[main] INFO org.foreach.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, поэтому его должно быть легко импортировать и запускать как есть.