1. 概述
本教程将演示如何更高效地使用Java读取大文件。
本文是《Java - 回归基础教程》系列教程之一。
2. 一次性读取所有行
一般做法是将文件中的所有行都读到内存中,我们可以使用Guava和Apache Commons IO提供的API快速实现:
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 Mb 消耗)
[main] INFO org.baeldung.java.CoreJavaIoUnitTest - Total Memory: 128 Mb
[main] INFO org.baeldung.java.CoreJavaIoUnitTest - Free Memory: 116 Mb
但,全部处理完后: (~2 Gb 消耗)
[main] INFO org.baeldung.java.CoreJavaIoUnitTest - Total Memory: 2666 Mb
[main] INFO org.baeldung.java.CoreJavaIoUnitTest - Free Memory: 490 Mb
约2.1 Gb的内存被消耗,因为文件中的所有行都被保存到内存中了,这会导致内存很快被消完。
很明显,将文件中的所有内容都读到内存中,会很快耗尽可用内存,不管实际内存是多少。
事实上,我们不需要将文件中的内容一次性都读到内存中,我们只需要能够遍历每行,处理完毕之后就可以丢弃。 因此,下面我们将遍历每一行,而不是将所有行都保存到内存中。
3. 文件流
下面我们介绍几种按位读取文件的方法。
3.1 使用 Scanner
使用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 Mb 消耗)
[main] INFO org.baeldung.java.CoreJavaIoUnitTest - Total Memory: 763 Mb
[main] INFO org.baeldung.java.CoreJavaIoUnitTest - Free Memory: 605 Mb
3.2 使用 BufferedReader
另一种方案是使用 BufferedReader 类实现。
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
while (br.readLine() != null) {
// do something with each line
}
}
BufferedReader 通过逐块读取文件并将这些块缓存在内部缓冲区中来减少I/O操作次数。
与 Scanner 相比,它表现出更好的性能,因为它只关注数据读取而不进行解析。
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
}
}
3.4 使用 SeekableByteChannel
SeekableByteChannel 提供读取和操作文件的管道。它比标准 I/O 类具有更快的性能,因为它由自动调整大小的字节数组支持。
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
Files.lines() 返回一个 String 流
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 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 Mb 消耗)
[main] INFO o.b.java.CoreJavaIoIntegrationTest - Total Memory: 752 Mb
[main] INFO o.b.java.CoreJavaIoIntegrationTest - Free Memory: 564 Mb
5. 总结
本文演示了如何在不消耗所有可用内存情况下,读取大文件。
文中所有示例和代码片段源码,可以从GitHub上获取。