Jakob Jenkov 2019-08-27
Java 的 Reader 类(java.io.Reader)是 Java IO API 中所有 Reader 子类的基类。与 Java InputStream 不同,Reader 是基于字符(character-based)而非字节(byte-based)的。换句话说,Reader 用于读取文本(字符),而 InputStream 用于读取原始字节。
Reader 与数据源
Reader 通常连接到某种数据源,如文件、字符数组、网络套接字等。这一点在 Java IO 概述 中有更详细的说明。
Unicode 中的字符
如今,许多应用程序使用 Unicode(UTF-8 或 UTF-16)来存储文本数据。在 UTF-8 中,一个字符可能需要一个或多个字节表示;而在 UTF-16 中,每个字符通常占用两个字节。因此,在读取文本数据时,单个字节未必对应一个完整的字符。如果你通过 InputStream 逐字节读取 UTF-8 数据并试图将每个字节转换为 char,结果可能不是你期望的文本。
为了解决这个问题,Java 提供了 Reader 类。Reader 能够将字节解码为字符。你需要在实例化 Reader(实际上是其子类)时指定要使用的字符集。
Java Reader 的子类
通常你不会直接使用 Reader,而是使用它的某个子类。Java IO 提供了多种 Reader 子类,包括:
- InputStreamReader
- CharArrayReader
- FileReader
- PipedReader
- BufferedReader
- FilterReader
- PushbackReader
- LineNumberReader
- StringReader
下面是一个创建 FileReader(Reader 的子类)的示例:
Reader reader = new FileReader("/path/to/file/thefile.txt");
从 Reader 中读取单个字符
Reader 的 read() 方法返回一个 int,其中包含下一个读取字符的 char 值。如果 read() 返回 -1,表示没有更多数据可读,此时可以关闭 Reader。注意:这里指的是 int 类型的 -1,而不是 byte 或 char 类型的 -1。
以下是从 Reader 中读取所有字符的示例:
Reader reader = new FileReader("/path/to/file/thefile.txt");
int theCharNum = reader.read();
while(theCharNum != -1) {
char theChar = (char) theCharNum;
System.out.print(theChar);
theCharNum = reader.read();
}
注意:代码首先读取一个字符,并检查其数值是否等于 -1。如果不是,则处理该字符并继续读取,直到 read() 返回 -1。
从 Reader 中读取字符数组
Reader 还提供了一个 read(char[], offset, length) 方法,它接受一个字符数组、起始偏移量和要读取的字符数量作为参数。字符将被读入该数组中从 offset 开始的位置,最多读取 length 个字符。
示例:
Reader reader = new FileReader("/path/to/file/thefile.txt");
char[] theChars = new char[128];
int charsRead = reader.read(theChars, 0, theChars.length);
while(charsRead != -1) {
System.out.println(new String(theChars, 0, charsRead));
charsRead = reader.read(theChars, 0, theChars.length);
}
该方法返回实际读入数组的字符数,若已到达数据末尾则返回 -1。
读取性能
一次读取一个字符数组比逐个字符读取要快得多,性能提升可达 10 倍甚至更多。
具体提速效果取决于字符数组大小、操作系统、硬件等因素。通常建议使用至少 8KB 的缓冲区。但超过系统底层缓存能力后,再增大数组也不会带来明显提升。你可能需要通过实验测量不同缓冲区大小下的读取性能,以找到最优值。
通过 BufferedReader 实现透明缓冲
你可以使用 Java BufferedReader 对 Reader 进行包装,从而自动实现高效缓冲。BufferedReader 会从底层 Reader 一次性读取一大块字符到内部缓冲区,然后你可以逐个字符地从 BufferedReader 读取,同时仍能获得批量读取带来的性能优势。
示例:
Reader input = new BufferedReader(
new FileReader("c:\\data\\input-file.txt"),
1024 * 1024 /* 缓冲区大小 */
);
注意:BufferedReader 本身也是 Reader 的子类,可以在任何需要 Reader 的地方使用。
跳过字符
Reader 提供了 skip(long n) 方法,用于跳过输入流中的若干字符。参数 n 表示要跳过的字符数。该方法返回实际跳过的字符数量(可能小于请求的数量,例如当剩余字符不足时)。
示例:
long charsSkipped = reader.skip(24);
关闭 Reader
读取完成后,应调用 close() 方法关闭 Reader:
reader.close();
或者,使用 Java 7 引入的 try-with-resources 语法自动关闭资源:
try(Reader reader = new FileReader("/path/to/file/thefile.txt")) {
int data = reader.read();
while(data != -1) {
System.out.print((char) data);
data = reader.read();
}
}
注意:这里不再需要显式调用 close(),try-with-resources 会自动处理。