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. 文件流

使用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

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上获取。