1. 逐行读取文件:BufferedReader 等
之前我们学习了按字节读取:这对处理二进制格式很方便,但对文本而言并不友好。文件由与编码相关的字符组成。因此 Java 提供了方便的“抽象层”——FileReader、BufferedReader 等类,它们把字节流转换为字符流和行。
想象一个文本文件——可能是程序日志、用户列表,或者甚至是巨著《战争与和平》。有时需要快速把整个文件读完,有时需要逐行遍历,有时则要提取某一特定的行。
在 Java 中有多种方式可用,选择取决于文件大小和任务。如果需要按行处理文件(例如统计行数或查找记录),就使用逐行读取。而如果文件较小——可以将其整体载入内存,并把它当作字符串列表来处理。
为什么逐行读取更好?
对于大文件,逐行读取能避免内存问题。把一个数 GB 的日志整读进内存是个坏主意:很容易触发 OutOfMemoryError。而按行读取则可以以很低的开销处理,即便文件有数百 MB。
在 Java 中如何实现?
最经典的方法——使用 BufferedReader(或其同类)与方法 readLine()。
import java.io.*;
public class ReadLinesDemo {
public static void main(String[] args) {
String fileName = "example.txt";
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
String line;
int lineNumber = 1;
while ((line = reader.readLine()) != null) {
System.out.printf("%3d: %s%n", lineNumber, line);
lineNumber++;
}
} catch (IOException e) {
System.out.println("读取文件时发生错误:" + e.getMessage());
}
}
}
这里我们打开文件进行读取,并通过 readLine() 逐行读取,直到返回 null(表示到达文件末尾)。之后将每一行连同行号输出。
简述 try-with-resources
看到 try(...){ ... } 这种结构了吗?这就是 try-with-resources。它保证即使在读取过程中发生错误,文件也会被关闭。你无需手动调用 close():即便有 catch/finally,关闭也会自动执行。
为什么需要 BufferedReader?
BufferedReader 并非逐字符读取,而是按块读取(通常为 8192 字节),这能加快文件读取。此外,它还提供了方便的 readLine() 方法,直接返回直到换行符之前的一整行。
应选择多大的缓冲区?
通常 BufferedReader 使用 8192 字节(8 KB)的缓冲区——对大多数任务已足够。如果你要读取非常长的行(例如每行 100_000 个字符),可以增大缓冲区:
BufferedReader reader = new BufferedReader(new FileReader(fileName), 65536); // 64 KB 缓冲区
但对于一般任务,标准大小就非常合适。
2. 整文件读取:Files.readAllLines 和 Files.readString
如果文件较小(例如不超过 10–20 MB),将其一次性读入很方便。例如,当你需要快速获取所有行列表、分析文本或通过网络发送它时。
现代方式:Files.readAllLines
自 Java 7 起引入了便捷的 Files 类,包含很多实用方法。
import java.nio.file.*;
import java.io.IOException;
import java.util.List;
public class ReadAllLinesDemo {
public static void main(String[] args) {
Path path = Path.of("example.txt");
try {
List<String> lines = Files.readAllLines(path);
for (int i = 0; i < lines.size(); i++) {
System.out.printf("%3d: %s%n", i + 1, lines.get(i));
}
} catch (IOException e) {
System.out.println("读取文件时发生错误:" + e.getMessage());
}
}
}
这里方法 Files.readAllLines(path) 返回一个字符串列表(List<String>)。可以像普通集合那样对它进行查找、排序、过滤。
更现代:Files.readString(Java 11+)
如果你需要将整个文件读取为一个字符串(例如用于查找子串或传给 JSON),请使用 Files.readString:
import java.nio.file.*;
import java.io.IOException;
public class ReadStringDemo {
public static void main(String[] args) {
Path path = Path.of("example.txt");
try {
String content = Files.readString(path);
System.out.println("文件内容:");
System.out.println(content);
} catch (IOException e) {
System.out.println("读取文件时发生错误:" + e.getMessage());
}
}
}
那编码怎么办?
默认情况下,Files.readAllLines 和 Files.readString 使用系统的默认编码。如果文件使用了其他编码(例如 Windows-1251),请显式指定:
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
String content = Files.readString(path, StandardCharsets.UTF_8);
3. 方案对比:什么时候用哪种方式
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
|
大文件,逐行处理 | 节省内存,灵活 | 只能按行读取 |
|
小型或中等大小的文件 | 直接得到字符串列表,简单 | 大文件可能导致 OutOfMemory |
|
小文件,需要整段文本 | 一次性得到完整字符串 | 没有按行拆分 |
建议:
— 如果文件较小——使用 Files.readAllLines 或 Files.readString。
— 如果文件很大或你不确定其大小——使用 BufferedReader.readLine()。
4. 实战:示例与解析
示例 1:统计大文件中的行数
假设你需要知道一个巨大文件(例如服务器日志)有多少行。
import java.io.*;
public class LineCount {
public static void main(String[] args) {
String fileName = "biglog.txt";
int lineCount = 0;
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
while (reader.readLine() != null) {
lineCount++;
}
System.out.println("文件总行数:" + lineCount);
} catch (IOException e) {
System.out.println("读取文件时发生错误:" + e.getMessage());
}
}
}
示例 2:按内容查找行
查找包含“错误”一词的所有行(不区分大小写):
import java.io.*;
public class FindErrorLines {
public static void main(String[] args) {
String fileName = "biglog.txt";
String keyword = "错误";
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
String line;
int lineNumber = 1;
while ((line = reader.readLine()) != null) {
if (line.toLowerCase().contains(keyword)) {
System.out.printf("%3d: %s%n", lineNumber, line);
}
lineNumber++;
}
} catch (IOException e) {
System.out.println("读取文件时发生错误:" + e.getMessage());
}
}
}
示例 3:从小型文件加载配置
文件 config.txt:
host=localhost
port=8080
mode=dev
把它全部读入并解析为键-值对:
import java.nio.file.*;
import java.util.*;
public class ConfigLoader {
public static void main(String[] args) throws Exception {
Path path = Path.of("config.txt");
List<String> lines = Files.readAllLines(path);
Map<String, String> config = new HashMap<>();
for (String line : lines) {
if (line.trim().isEmpty() || line.startsWith("#")) continue; // 跳过空行和注释
String[] parts = line.split("=", 2);
if (parts.length == 2) {
config.put(parts[0].trim(), parts[1].trim());
}
}
System.out.println("已加载的配置:" + config);
}
}
5. 读取文本文件的常见错误
错误 1:尝试将二进制文件当作文本读取。 如果你使用 BufferedReader 或 Files.readAllLines 打开图像或压缩包,会得到莫名其妙的字符,并且有触发 OutOfMemoryError 的风险。对二进制文件请使用 InputStream!
错误 2:未处理异常。 文件可能被删除、移动、或被锁定。始终用 try-catch 包裹读取,并告知用户出现的问题。
错误 3:忽略编码。 如果你的文本是俄文,而你在读取文件时未指定编码,可能会得到“?????”。请使用 StandardCharsets.UTF_8 或所需的编码。
错误 4:忘记关闭流。 如果不关闭文件,它可能会一直被锁定直到程序结束。务必使用 try-with-resources。
错误 5:在文本文件中使用 read() 而不是 readLine()。 方法 read() 按字符读取——这对按行处理既慢又不方便。处理文本请使用 readLine() 或 Files 类的方法。
GO TO FULL VERSION