Skip to content

Reader / Writer 父类

你知道 Java 的 IO 类为什么分成「字节流」和「字符流」两套吗?

表面上看,InputStream/OutputStreamReader/Writer 做着类似的事——读写数据。但实际上,它们的设计哲学完全不同:前者处理原始字节,后者处理 Unicode 字符。

这种区分,是因为 Java 在 1.1 引入了国际化支持。当你需要正确处理中文、日文、emoji 时,字符流是不可绕过的话题。

Reader 的方法签名解析

Reader 是所有字符输入流的抽象父类。它的方法签名,藏着很多设计细节:

java
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 是所有字符输出流的抽象父类:

java
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 支持链式调用:

java
writer.append("Hello").append(' ').append("World");

字节流 vs 字符流:核心差异

对比维度字节流字符流
处理单位字节(byte)字符(char,16 位 Unicode)
根类InputStream / OutputStreamReader / Writer
返回类型int(字节值 0~255)int(字符 Unicode 值)
数组参数byte[]char[]
直接写字符串❌ 不支持write(String)
编码处理有(自动处理字节↔字符转换)

为什么需要字符流:看一个乱码问题

java
// ❌ 用字节流读中文:需要手动处理编码
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()。它们的语义不同:

java
// ready():判断是否可以在不阻塞的情况下读取至少一个字符
if (reader.ready()) {
    int c = reader.read(); // 有数据,不会阻塞
}

// available() 是字节流的,返回预估可读字节数
int available = fis.available(); // 字节流才有

关键区别:即使 ready() 返回 trueread() 仍可能阻塞(比如从网络读取)。ready() 只是告诉你「目前没有阻塞」,不是「数据一定存在」。

关闭流的正确姿势

装饰器模式下,只关闭最外层流即可,底层流会被自动关闭:

java
// ❌ 只关外层不够?错!
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 最稳当。

基于 VitePress 构建