1. 简介

本文将探讨如何使用Java标准库和Apache Commons IO库读取文件的最后N行内容。我们将对比不同实现方式的优缺点,帮助你根据实际场景选择最佳方案。

2. 测试数据准备

为统一演示效果,所有示例将使用以下测试数据:

首先创建测试文件 data.txt

line 1
line 2
line 3
line 4
line 5
line 6
line 7
line 8
line 9
line 10

定义测试参数:

private static final String FILE_PATH = "src/test/resources/data.txt";
private static final int LAST_LINES_TO_READ = 3;
private static final String OUTPUT_TO_VERIFY = "line 8\nline 9\nline 10";

3. 使用 BufferedReader 方案

BufferedReader 提供逐行读取文件的能力,核心优势是无需将整个文件加载到内存。配合队列(FIFO结构)实现动态维护最后N行:

@Test
public void givenFile_whenUsingBufferedReader_thenExtractedLastLinesCorrect() throws IOException {
    try (BufferedReader br = Files.newBufferedReader(Paths.get(FILE_PATH))) {
        Queue<String> queue = new LinkedList<>();
        String line;
        while ((line = br.readLine()) != null){
            if (queue.size() >= LAST_LINES_TO_READ) {
                queue.remove(); // 队列满时移除最早元素
            }
            queue.add(line);
        }

        assertEquals(OUTPUT_TO_VERIFY, String.join("\n", queue));
    }
}

⚠️ 适用场景:大文件处理,内存敏感型应用

4. 使用 Scanner 方案

Scanner 提供类似的逐行读取能力,实现逻辑与 BufferedReader 基本一致:

@Test
public void givenFile_whenUsingScanner_thenExtractedLastLinesCorrect() throws IOException {
    try (Scanner scanner = new Scanner(new File(FILE_PATH))) {
        Queue<String> queue = new LinkedList<>();
        while (scanner.hasNextLine()){
            if (queue.size() >= LAST_LINES_TO_READ) {
                queue.remove();
            }
            queue.add(scanner.nextLine());
        }

        assertEquals(OUTPUT_TO_VERIFY, String.join("\n", queue));
    }
}

性能提示:相比 BufferedReaderScanner 在处理大文件时性能稍差

5. 使用 NIO2 Files 方案

Java 7+ 的 Files 类通过 lines() 方法提供流式处理能力,特别适合大文件场景:

@Test
public void givenLargeFile_whenUsingFilesAPI_thenExtractedLastLinesCorrect() throws IOException{
    try (Stream<String> lines = Files.lines(Paths.get(FILE_PATH))) {
        Queue<String> queue = new LinkedList<>();
        lines.forEach(line -> {
            if (queue.size() >= LAST_LINES_TO_READ) {
                queue.remove();
            }
            queue.add(line);
        });

        assertEquals(OUTPUT_TO_VERIFY, String.join("\n", queue));
    }
}

优势:流式处理,内存占用可控,支持并行处理

6. 使用 Apache Commons IO 方案

6.1 FileUtils 实现

FileUtils.readLines() 会将整个文件加载到内存,仅推荐用于小文件

@Test
public void givenFile_whenUsingFileUtils_thenExtractedLastLinesCorrect() throws IOException{
    File file = new File(FILE_PATH);
    List<String> lines = FileUtils.readLines(file, "UTF-8");
    StringBuilder stringBuilder = new StringBuilder();
    for (int i = (lines.size() - LAST_LINES_TO_READ); i < lines.size(); i++) {
        stringBuilder.append(lines.get(i)).append("\n");
    }

    assertEquals(OUTPUT_TO_VERIFY, stringBuilder.toString().trim());
}

踩坑警告:处理大文件时极易触发 OOM

6.2 ReversedLinesFileReader 实现

ReversedLinesFileReader 提供反向读取能力,直接定位到文件末尾,效率极高:

@Test
public void givenFile_whenUsingReverseFileReader_thenExtractedLastLinesCorrect() throws IOException{
    File file = new File(FILE_PATH);
    try (ReversedLinesFileReader rlfReader = new ReversedLinesFileReader(file, StandardCharsets.UTF_8)) {
        List<String> lastLines = rlfReader.readLines(LAST_LINES_TO_READ);
        StringBuilder stringBuilder = new StringBuilder();
        Collections.reverse(lastLines); // 反转恢复原始顺序
        lastLines.forEach(
          line -> stringBuilder.append(line).append("\n")
        );

        assertEquals(OUTPUT_TO_VERIFY, stringBuilder.toString().trim());
    }
}

最佳实践:处理超大文件的首选方案,时间复杂度接近O(1)

7. 方案对比与选型建议

方案 内存占用 CPU消耗 适用场景
BufferedReader 大文件,内存敏感
Scanner 简单场景,小文件
NIO2 Files 大文件,需要流式处理
FileUtils 小文件,简单实现
ReversedLinesReader 极低 极低 超大文件,性能敏感

选型决策树

  1. 文件大小 < 1MB → FileUtils(简单粗暴)
  2. 1MB < 文件大小 < 100MB → BufferedReaderNIO2 Files
  3. 文件大小 > 100MB → ReversedLinesReader(性能王者)

所有示例代码可在 GitHub仓库 获取。


原始标题:Read Last N Lines From File in Java | Baeldung