首页 / Java 入门教程 / 字符流

Java 入门教程

字符流

本教程共 100 篇 · 第 87 篇 · 更新于 2026-08-05 · 约 5 分钟阅读

JavaJava 入门教程字符流ReaderWriter字符编码

本节目标:用 Reader 和 Writer 正确读写文本文件,彻底搞明白中文乱码是怎么来的、又该怎么根治。

为什么需要字符流

上一章的字节流已经能读写任何文件了。既然如此,为什么还要单独搞一套字符流?

因为文本有编码问题。

Java 内部用 Unicode 存字符,一个 char 占 16 位。但文件里存的是字节。UTF-8 编码下,一个英文字母占 1 字节,一个汉字占 3 字节。

用字节流读中文文本,你拿到的是一堆零散字节,得自己拼装、自己解码。字符流把这活儿全包了。

字符流 = 字节流 + 自动编码转换。

Reader 与 Writer

字符流的两个抽象基类是 ReaderWriter,方法签名跟字节流长得很像:

public int read() throws IOException;   // Reader
public void write(int c) throws IOException; // Writer

Reader.read() 同样返回 int,同样用 -1 表示末尾。区别在于:这个 int 的低 16 位装的是一个 char,而不是一个 byte

对应到文件读写,实现类是 FileReaderFileWriter

第一个字符流示例

import java.io.*;
import java.nio.charset.StandardCharsets;

public class CharDemo {
    public static void main(String[] args) throws IOException {
        try (Writer writer = new FileWriter("poem.txt", StandardCharsets.UTF_8)) {
            writer.write("床前明月光\n");
            writer.write("疑是地上霜\n");
        }

        try (Reader reader = new FileReader("poem.txt", StandardCharsets.UTF_8)) {
            int c;
            while ((c = reader.read()) != -1) {
                System.out.print((char) c);
            }
        }
    }
}

运行:

javac CharDemo.java
java CharDemo

输出:

床前明月光
疑是地上霜

注意 write(String) 这个重载——Writer 可以直接写字符串,不用先转成数组,这比字节流方便太多。

一定要显式指定字符集

上面代码里的 StandardCharsets.UTF_8 不是可有可无的装饰。

FileReaderFileWriter 如果不传字符集,会使用运行环境的默认字符集。这个默认值在不同机器上不一样:Linux 和 macOS 通常是 UTF-8,中文 Windows 历史上长期是 GBK。

结果就是:你在自己电脑上跑得好好的程序,换台机器就乱码。

Warning

「本机正常、别人电脑乱码」几乎百分之百是没指定字符集。养成习惯:只要涉及文本读写,永远显式写上字符集。

Note

带字符集参数的 FileReader(File, Charset)FileWriter(File, Charset) 构造方法是 Java 11 引入的。Java 11 之前只能用后面第 88 章讲的 InputStreamReader 来指定编码。

从 Java 18 起,file.encoding 的默认值已统一为 UTF-8,情况好了很多。但显式指定依然是最稳妥的做法。

用缓冲区批量读

跟字节流一样,逐字符读效率低。Reader 提供了 read(char[] cbuf)

import java.io.*;
import java.nio.charset.StandardCharsets;

public class CharBuffer {
    public static void main(String[] args) throws IOException {
        try (Writer w = new FileWriter("article.txt", StandardCharsets.UTF_8)) {
            for (int i = 0; i < 500; i++) {
                w.write("Java 字符流练习。");
            }
        }

        try (Reader reader = new FileReader("article.txt", StandardCharsets.UTF_8)) {
            char[] buffer = new char[256];
            int n;
            int total = 0;
            while ((n = reader.read(buffer)) != -1) {
                total += n; // n 是本次读到的字符数
            }
            System.out.println("共读取 " + total + " 个字符");
        }
    }
}

输出:

共读取 5000 个字符

500 次循环,每次 10 个字符,正好 5000。注意这里统计的是字符数不是字节数——同样的文件用字节流读,会得到大得多的数字。

字符流与字节流对照

把两套体系放在一起比,差异一目了然:

对比项字节流字符流
基类InputStream / OutputStreamReader / Writer
最小单位字节(8 位)字符(16 位)
缓冲数组byte[]char[]
编码转换不做,原样搬运自动完成
适用数据图片、视频、压缩包文本文件、源码、配置
直接写字符串不支持支持 write(String)

选型规则依然是那句话:能用文本编辑器打开看懂的,用字符流;打开是乱码的,用字节流。

Writer 的追加与刷新

FileWriter 也支持追加模式,参数位置在字符集之后:

// 追加写入,不覆盖原内容
try (Writer w = new FileWriter("log.txt", StandardCharsets.UTF_8, true)) {
    w.write("新的一行\n");
}

Writer 同样有 flush() 方法。字符流内部为了做编码转换,天然带一小块缓冲,不 flush 也不 close 的话数据可能停在内存里。

用 try-with-resources 就不用操心这个问题,离开代码块自动关闭并刷新。

内存中的字符流

除了文件,字符流也有内存版实现。

StringReader 把一个字符串包装成 ReaderCharArrayWriter 把写入的字符收集到内存数组:

import java.io.*;

public class MemoryChar {
    public static void main(String[] args) throws IOException {
        String source = "内存里的文本";

        try (Reader reader = new StringReader(source);
             CharArrayWriter writer = new CharArrayWriter()) {
            int c;
            while ((c = reader.read()) != -1) {
                writer.write(c);
            }
            System.out.println(writer.toString());
        }
    }
}

输出:

内存里的文本

这类内存流在测试中很有价值。你写的解析方法如果接收 Reader 参数,测试时喂个 StringReader 就行,不必准备真实文件。

一个常见误区

有人觉得字符流「更高级」,于是拿它读图片。这会出大问题。

字符流会尝试把字节按编码规则解释成字符。图片的字节序列不符合任何文本编码,非法字节会被替换成占位符 \uFFFD。这个替换是不可逆的——写回去的文件已经损坏了。

Warning

二进制文件绝对不能过字符流。复制图片、视频、.class 文件,只能用字节流。

编码不一致会怎样

编码问题是新手最容易踩的坑,值得单独演示一次。

同一段中文,用 UTF-8 写出去,再用 GBK 读回来,结果一定是乱的:

import java.io.*;
import java.nio.charset.*;

public class CharsetMismatch {
    public static void main(String[] args) throws IOException {
        try (Writer w = new FileWriter("note.txt", StandardCharsets.UTF_8)) {
            w.write("中文测试");
        }

        // 故意用错误的字符集读取
        try (Reader r = new FileReader("note.txt", Charset.forName("GBK"))) {
            char[] buf = new char[64];
            int n = r.read(buf);
            System.out.println("GBK 读出:" + new String(buf, 0, n));
        }

        try (Reader r = new FileReader("note.txt", StandardCharsets.UTF_8)) {
            char[] buf = new char[64];
            int n = r.read(buf);
            System.out.println("UTF-8 读出:" + new String(buf, 0, n));
        }
    }
}

输出大致是这样:

GBK 读出:涓枃娴嬭瘯
UTF-8 读出:中文测试

文件本身没有任何问题,出错的只是解读方式。这就像同一串摩斯电码,用错了码表自然翻译不出原话。

Note

文本文件不会记录自己是什么编码(BOM 是个不完全可靠的例外)。编码信息只存在于「约定」里,所以代码中必须写死。团队里统一用 UTF-8,能省掉九成的乱码事故。

字符与字节的数量关系

还有个细节容易混淆:字符数不等于字节数。

ASCII 字符在 UTF-8 下占 1 个字节,常用汉字占 3 个字节,部分生僻字和 emoji 占 4 个字节。所以 "abc中文" 是 5 个字符,却占 9 个字节。

用字符流读到的 n 是字符个数,用字节流读到的 n 是字节个数,两者不能互相换算。做长度校验、进度条、分片上传时,务必想清楚统计的到底是哪一个。

本节小结

  • 字符流是「字节流 + 编码转换」,专门处理文本。
  • Reader.read() 返回的 int 里装的是 char-1 仍表示结束。
  • 读写文本一律显式传 StandardCharsets.UTF_8,否则换机器就乱码。
  • FileReader/FileWriter 的字符集参数从 Java 11 才有。
  • 批量读用 char[],统计的是字符数不是字节数。
  • 二进制文件只能用字节流,过字符流必定损坏。