Reader / Writer 父类
你知道 Java 的 IO 类为什么分成「字节流」和「字符流」两套吗?
表面上看,InputStream/OutputStream 和 Reader/Writer 做着类似的事——读写数据。但实际上,它们的设计哲学完全不同:前者处理原始字节,后者处理 Unicode 字符。
这种区分,是因为 Java 在 1.1 引入了国际化支持。当你需要正确处理中文、日文、emoji 时,字符流是不可绕过的话题。
Reader 的方法签名解析
Reader 是所有字符输入流的抽象父类。它的方法签名,藏着很多设计细节:
public abstract class Reader implements Readable, Closeable {
// 单字符读取(子类必须实现)
public abstract int read() throws IOException;
// 批量读取到字符数组
public int read(char[] cbuf) throws IOException {
return read(cbuf, 0, cbuf.length);
}
// 读取到字符数组指定区间
public int read(char[] cbuf, int off, int len) throws IOException;
// 读一行(仅 BufferedReader 有实现)
public String readLine() throws IOException {
throw new UnsupportedOperationException();
}
// 跳过 n 字符
public long skip(long n) throws IOException;
// 标记与重置
public boolean markSupported();
public void mark(int readAheadLimit) throws IOException;
public void reset() throws IOException;
// 是否准备好读取
public boolean ready() throws IOException;
// 关闭
public void close() throws IOException;
}注意到 readLine() 了吗?父类里直接抛异常,但 BufferedReader 重写了它。这正是模板方法模式的应用——父类定义骨架,子类提供具体实现。
read() 返回值的设计意图
| 返回值 | 含义 |
|---|---|
正整数 n | 成功读取 n 个字符 |
-1 | 流已结束,无更多数据 |
这里有个容易混淆的点:返回值是 int 而不是 char。因为 char 是无符号的 16 位,而 int 是 32 位,能用 -1 表示流结束。如果返回值是 char,就无法区分「读取到字符 0xFFFF」和「流结束」了。
Writer 的方法签名解析
Writer 是所有字符输出流的抽象父类:
public abstract class Writer implements Appendable, Closeable, Flushable {
// 写单个字符
public abstract void write(int c) throws IOException;
// 写字符数组(三个重载)
public void write(char[] cbuf) throws IOException {
write(cbuf, 0, cbuf.length);
}
public abstract void write(char[] cbuf, int off, int len) throws IOException;
// 直接写 String(这是字节流没有的能力)
public void write(String str) throws IOException {
write(str, 0, str.length());
}
public void write(String str, int off, int len) throws IOException;
// 追加模式(实现 Appendable 接口)
public Writer append(CharSequence csq) throws IOException {
write(String.valueOf(csq));
return this;
}
// 换行(BufferedWriter 的特权)
public void newLine() throws IOException {
throw new UnsupportedOperationException();
}
// 刷新与关闭
public abstract void flush() throws IOException;
public abstract void close() throws IOException;
}Appendable 接口让 Writer 支持链式调用:
writer.append("Hello").append(' ').append("World");字节流 vs 字符流:核心差异
| 对比维度 | 字节流 | 字符流 |
|---|---|---|
| 处理单位 | 字节(byte) | 字符(char,16 位 Unicode) |
| 根类 | InputStream / OutputStream | Reader / Writer |
| 返回类型 | int(字节值 0~255) | int(字符 Unicode 值) |
| 数组参数 | byte[] | char[] |
| 直接写字符串 | ❌ 不支持 | ✅ write(String) |
| 编码处理 | 无 | 有(自动处理字节↔字符转换) |
为什么需要字符流:看一个乱码问题
// ❌ 用字节流读中文:需要手动处理编码
FileInputStream fis = new FileInputStream("chinese.txt");
byte[] buf = new byte[1024];
int len = fis.read(buf);
// 字节数组到手,你需要知道它是什么编码
String s = new String(buf, 0, len, StandardCharsets.UTF_8);
// ✅ 用字符流读中文:自动处理编码
FileReader fr = new FileReader("chinese.txt");
char[] buf = new char[1024];
int len = fr.read(buf);
// buf 里已经是字符了,拿来就能用字符流的本质是:在字节流的基础上,加了一层编码转换器。
ready() vs available()
字节流有 available(),字符流有 ready()。它们的语义不同:
// ready():判断是否可以在不阻塞的情况下读取至少一个字符
if (reader.ready()) {
int c = reader.read(); // 有数据,不会阻塞
}
// available() 是字节流的,返回预估可读字节数
int available = fis.available(); // 字节流才有关键区别:即使 ready() 返回 true,read() 仍可能阻塞(比如从网络读取)。ready() 只是告诉你「目前没有阻塞」,不是「数据一定存在」。
关闭流的正确姿势
装饰器模式下,只关闭最外层流即可,底层流会被自动关闭:
// ❌ 只关外层不够?错!
BufferedReader br = new BufferedReader(
new FileReader("file.txt"));
br.close(); // 只关了 BufferedReader?
// 实际上:BufferedReader.close() 会关闭底层 FileReader
// 这是 JDK 帮你做的
// ✅ 只需要关最外层
try (
BufferedReader br = new BufferedReader(
new FileReader("file.txt"))
) {
// 读取
}
// try-with-resources 确保正确关闭BufferedReader / BufferedWriter 会自动关闭底层流。这是装饰器模式带来的福利。
记住这个口诀:
字符流读 char,字节流读 byte; 写字符串用字符流,底层自动转编码; 装饰器只关外层,try-with-resources 最稳当。
