一般来说读取文件行的标准方法是保存在内存中读取。 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 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 的内存 - 原因很简单 - 文件的行现在都存储在内存中。
很明显,将文件内容保留在内存中将很快耗尽可用内存。
更重要的是,我们通常不需要一次将文件中的所有行都存储在内存中- 相反,我们只需要能够迭代每一行,进行一些处理然后将其丢弃。所以,读取大文件正确方式应该是 迭代这些行而不将它们全部保存在内存中。
下面我们来看一下在Java中有哪些方式可以高效读取大文件。
- 使用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
使用BufferedReader类
此类提供了readLine()方法来缓冲字符,简化了读取文件的过程,该方法会逐行读取给定文件的内容。
try (BufferedReader br = new BufferedReader(new FileReader(fileName))) {
while (br.readLine() != null) {
// do something with each line
}
}
BufferedReader通过逐块读取文件并将块缓存在内部缓冲区中来减少 I/O 操作的数量。
与Scanner相比,它表现出更好的性能,因为它只专注于数据检索而不进行解析。
使用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实例的方法。
使用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()方法显式指定缓冲区大小。
使用Stream API
同样,我们可以使用Stream API来读取和处理文件的内容。
在这里,我们将使用Files类,它提供lines()方法来返回String元素流。
请注意,文件是延迟处理的,这意味着在给定时间只有部分内容存储在内存中。
try (Stream lines = java.nio.file.Files.lines(Paths.get(fileName))) {
lines.forEach(line -> {
// do something with each line
});
}
使用 Apache 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
- 总结
在处理大型文件时,我们需要采取一些策略来避免迭代和耗尽可用内存。通过使用流式处理、分块处理等技术,我们可以更有效地利用系统资源,并快速、准确地处理大型文件。以上这些方式处理大型文件时非常有用。