在Java中高效读取超大文件的几种方式

在Java中高效读取超大文件的几种方式

编码文章call10242025-02-01 3:14:2815A+A-

一般来说读取文件行的标准方法是保存在内存中读取。 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
  • 总结

在处理大型文件时,我们需要采取一些策略来避免迭代和耗尽可用内存。通过使用流式处理、分块处理等技术,我们可以更有效地利用系统资源,并快速、准确地处理大型文件。以上这些方式处理大型文件时非常有用。

点击这里复制本文地址 以上内容由文彬编程网整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!
qrcode

文彬编程网 © All Rights Reserved.  蜀ICP备2024111239号-4