1. 概述
本文将介绍在 Java 中如何高效地读取文件中指定行号的某一行内容。这在处理日志、配置文件或大数据文件时非常实用。我们会对比几种常见方式,帮你避开踩坑,选择最合适的方法。
2. 测试文件准备
我们先准备一个名为 inputLines.txt
的测试文件,内容如下,供后续所有示例使用:
Line 1
Line 2
Line 3
Line 4
Line 5
文件路径假设为:/tmp/inputLines.txt
(实际使用时请替换为真实路径)。
3. 使用 BufferedReader
BufferedReader
是传统但高效的选择,✅ 适合大文件,因为它不会一次性把整个文件加载进内存。
核心思路:逐行读取,跳过前 N-1 行,读取第 N 行。
@Test
public void givenFile_whenUsingBufferedReader_thenExtractedLineIsCorrect() {
String filePath = "/tmp/inputLines.txt";
try (BufferedReader br = Files.newBufferedReader(Paths.get(filePath))) {
// 跳过前3行(读取但不使用)
for (int i = 0; i < 3; i++) {
br.readLine();
}
// 读取第4行(行号从1开始)
String extractedLine = br.readLine();
assertEquals("Line 4", extractedLine);
} catch (IOException e) {
fail("文件读取失败: " + e.getMessage());
}
}
⚠️ 注意:readLine()
返回 null
表示已到文件末尾,使用前需判空。
4. 使用 Scanner
Scanner
也能实现类似功能,API 更简洁,但 ⚠️ 性能不如 BufferedReader,尤其是在大文件场景。
@Test
public void givenFile_whenUsingScanner_thenExtractedLineIsCorrect() {
String filePath = "/tmp/inputLines.txt";
try (Scanner scanner = new Scanner(new File(filePath))) {
// 跳过前3行
for (int i = 0; i < 3; i++) {
scanner.nextLine();
}
// 读取第4行
String extractedLine = scanner.nextLine();
assertEquals("Line 4", extractedLine);
} catch (FileNotFoundException e) {
fail("文件未找到: " + e.getMessage());
}
}
✅ 优点:语法直观,适合简单场景。
❌ 缺点:内部做了额外的解析工作(如类型匹配),缓冲区较小,大文件下性能较差。
5. 使用 Files 工具类(NIO.2)
Java 7 引入的 java.nio.file.Files
提供了更现代的 API,分为两种使用方式:
5.1 小文件:readAllLines()
适用于小文件,简单粗暴,直接把所有行读入 List<String>
。
@Test
public void givenSmallFile_whenUsingFilesAPI_thenExtractedLineIsCorrect() {
String filePath = "/tmp/inputLines.txt";
try {
// 读取所有行,索引从0开始
List<String> lines = Files.readAllLines(Paths.get(filePath));
String extractedLine = lines.get(4); // 第5行
assertEquals("Line 5", extractedLine);
} catch (IOException e) {
fail("读取失败: " + e.getMessage());
}
}
⚠️ 警告:大文件慎用!会直接 OOM。
5.2 大文件:Files.lines() + Stream
✅ 推荐用于大文件,返回 Stream<String>
,支持函数式编程,且是惰性加载。
@Test
public void givenLargeFile_whenUsingFilesAPI_thenExtractedLineIsCorrect() {
String filePath = "/tmp/inputLines.txt";
try (Stream<String> lines = Files.lines(Paths.get(filePath))) {
String extractedLine = lines
.skip(4) // 跳过前4行
.findFirst() // 获取下一行(第5行)
.orElse(null); // 防止空指针
assertEquals("Line 5", extractedLine);
} catch (IOException e) {
fail("流处理失败: " + e.getMessage());
}
}
✅ 优势:内存友好,支持链式操作,可结合 filter、map 等操作。
⚠️ 注意:Stream
必须用 try-with-resources
关闭,否则会资源泄漏。
6. 使用 Apache Commons IO
如果你的项目已经引入了 commons-io
,那 FileUtils
和 IOUtils
也能快速实现。
6.1 FileUtils.readLines()
将文件全部读入 List<String>
,和 Files.readAllLines()
类似。
@Test
public void givenFile_whenUsingFileUtils_thenExtractedLineIsCorrect() {
File file = new File("/tmp/inputLines.txt");
try {
List<String> lines = FileUtils.readLines(file, "UTF-8");
String extractedLine = lines.get(0); // 第1行
assertEquals("Line 1", extractedLine);
} catch (IOException e) {
fail("读取失败: " + e.getMessage());
}
}
⚠️ 同样不适用于大文件。
6.2 IOUtils.toString()
读取整个文件为 String
,然后手动按换行符分割。
@Test
public void givenFile_whenUsingIOUtils_thenExtractedLineIsCorrect() {
String filePath = "/tmp/inputLines.txt";
try {
String fileContent = IOUtils.toString(new FileInputStream(filePath), StandardCharsets.UTF_8);
String[] lines = fileContent.split(System.lineSeparator());
String extractedLine = lines[0]; // 第1行
assertEquals("Line 1", extractedLine);
} catch (IOException e) {
fail("转换失败: " + e.getMessage());
}
}
❌ 不推荐:不仅内存占用高,split(System.lineSeparator())
在跨平台时可能出问题(Windows \r\n
vs Unix \n
)。
7. 总结
方法 | 适用场景 | 内存占用 | 推荐指数 |
---|---|---|---|
BufferedReader |
大文件、性能敏感 | ✅ 低 | ⭐⭐⭐⭐ |
Scanner |
小文件、简单脚本 | ❌ 较高 | ⭐⭐ |
Files.readAllLines() |
小文件、代码简洁 | ❌ 高 | ⭐⭐⭐ |
Files.lines() |
大文件、函数式风格 | ✅ 低 | ⭐⭐⭐⭐⭐ |
FileUtils.readLines() |
小文件、已有 commons-io | ❌ 高 | ⭐⭐⭐ |
IOUtils.toString() |
不推荐 | ❌ 极高 | ⭐ |
✅ 最终建议:
- 小文件:用
Files.readAllLines()
,简单直接。 - 大文件:首选
Files.lines()
+ Stream,兼顾性能与可读性。 - 老项目:
BufferedReader
依然可靠。
所有示例代码已上传至 GitHub:https://github.com/java-dev-tips/file-line-reader-examples