← 返回题目列表

Java 字节流和字符流有什么区别?缓冲流有什么作用?

高频 简单 第 2 / 22 题 更新于 2026/07/25
InputStreamReader缓冲流字符集

简化版

字节流以字节为单位,适合图片、音视频等任意二进制数据;字符流会按字符集在字节和字符之间编解码,适合文本。缓冲流通过成批读写减少底层 I/O 调用次数,通常比逐字节、逐字符操作更高效。

详细版

Java 经典 I/O 有两套核心抽象:

  • InputStream / OutputStream 处理原始字节,不关心数据表示的是文字还是图片。
  • Reader / Writer 处理 Unicode 字符,它们在读写文本时必须使用字符集完成编解码。

InputStreamReader 是从字节流到字符流的桥梁,OutputStreamWriter 则是反方向的桥梁。处理文本时应显式指定字符集,例如 StandardCharsets.UTF_8,让文件协议不依赖 JDK 版本、启动配置或运行环境的默认字符集。

BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter 会在内存中维护缓冲区。它们不会改变数据类型,只是将多次小额读写合并成较少的底层操作。BufferedReader 还提供了按行读取的能力。

完整版教学

一、为什么文本不能直接当字节处理

文件、网络和磁盘底层传输的都是字节,但一个字符可能由一个或多个字节表示。例如 UTF-8 中的中文通常占多个字节。如果将一批字节在字符边界中间截断,然后直接逐块构造字符串,就可能得到乱码。

InputStreamReader 内部的解码器会保留未组成完整字符的字节,等下一批数据到来后继续解码。这就是处理文本时应使用 Reader 体系或显式使用 CharsetDecoder 的原因。

Path path = Path.of("article.txt");

try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

二、怎么选流

  • 处理图片、压缩包、PDF、序列化数据:使用字节流,不要经过 Reader / Writer
  • 处理已知字符集的文本:使用 Reader / Writer,并显式指定字符集。
  • 读取文本行:使用 BufferedReaderFiles.lines
  • 按 Java 基本类型读写二进制数据:可使用 DataInputStream / DataOutputStream,但读写顺序必须一致。

三、缓冲为什么能提速

每次调用底层文件或网络 I/O 都可能涉及用户态与内核态切换。如果逐字节读取一个大文件,调用次数会非常多。缓冲流一次从底层读入一批数据,之后的小额读取大多只访问内存。

缓冲不是越大越好。过大会增加内存占用,对延迟和吞吐量的影响也要用实际负载测量。大多数普通文件读写使用 JDK 默认值即可。

四、常见易错点

  • flush() 是将 Java 缓冲区中的数据推向底层输出,不等于数据已经物理落盘。
  • close() 通常会先刷新再关闭,但仍应使用 try-with-resources 确保异常路径也能释放资源。
  • 不要用 InputStream.available() 猜测整个文件或整条消息的长度,它只是下一次读取可以不阻塞地获取到的字节数估计。

五、字符集错误、BOM 与换行边界

字符解码不仅要选字符集,还要决定遇到非法字节时怎么处理。便捷构造器通常使用解码器的默认错误动作;对数据导入、协议网关等需要严格质量控制的场景,可以显式创建 CharsetDecoder,将 malformed 或 unmappable 输入配置为报告错误,而不是悄悄接受替代字符。

UTF-8 的 BOM 不是必须项,有些读取方式会把它解码为字符串开头的 U+FEFF。跨系统文本还可能使用 \n\r\n\r 换行,所以业务若要求逐字节一致,不能先按文本读取再写回;字符流适合语义文本,不适合保持原始编码字节。

需求合适抽象关键边界
原样复制 PNG字节流不做字符解码
读取 UTF-8 CSVReader明确字符集与异常策略
保持原文件字节完全一致字节流不规范化换行或 BOM
写文本协议Writer明确编码、换行和 flush 时机

六、用数字理解缓冲收益与上限

假设读取 8 MiB 文件。逐字节调用意味着约 8,388,608 次 Java 读取操作;使用 8 KiB 缓冲区,理想情况下底层填充次数约为 8 MiB / 8 KiB = 1,024 次。真实性能还受文件系统页缓存、设备预读和 JDK 实现影响,但“把小调用合成批量调用”的方向不会变。

底层填充次数 ≈ ceil(文件字节数 / 缓冲区大小)

把缓冲区从 8 KiB 增大到 8 MiB 并不保证再快 1,024 倍,因为系统调用并非唯一成本,而且大缓冲会增加并发连接的内存占用。若 10,000 个连接各保留 64 KiB 读缓冲,仅这部分就约为 625 MiB,因此容量应结合并发数和实测吞吐选择。

记忆钩子:字节/字符决定“数据如何解释”,缓冲决定“每次搬多少”;两者是正交问题,BufferedReader 依然是字符流。

关闭顺序也属于正确性:Writer 可能持有尚未编码或尚未下推的字符,正常关闭会先完成这些工作。发生异常后应依赖 try-with-resources,同时保留最初异常及 close 产生的 suppressed exception,不能只写一个空 catch。

如果协议要求固定字节长度,长度应在编码后计算。字符串有 10 个 char 不代表 UTF-8 一定是 10 字节,直接用 text.length() 填网络帧长度会在中文或 emoji 上出错。

编码器也可能在关闭或结束输入时输出剩余字节,因此不能在 Writer 尚未完成时就读取底层 ByteArrayOutputStream 的最终协议结果。正确做法是按 API 生命周期完成 flush/close,或显式驱动 CharsetEncoder 的结束阶段。

七、常见误区与追问

  • 误区:字符流在内存里直接保存 UTF-8 字节。 Reader/Writer 面向 Java 字符,边界处需要按 Charset 解码或编码。
  • 误区:一个 Java char 永远等于一个用户看到的字符。 char 是 UTF-16 code unit,补充字符可能需要代理对,组合字符还可能由多个 code point 构成。
  • 误区:缓冲区越大性能一定越好。 超过合适批量后收益会递减,并发场景还会放大内存占用。
  • 误区:flush() 等于数据已经安全落盘。 flush 主要把 Java 层缓冲向下推送,持久化保证还涉及文件系统、设备缓存及相应 force/fsync 语义。
  • 追问:为什么不能用 available() 取得完整消息长度? 它只估计下一次读取无需阻塞可取得的字节数,不表达文件总长或应用协议边界。
  • 追问:关闭包装流会发生什么? 通常会连带关闭被包装的底层流,因此共享底层资源时必须明确所有权。
  • 追问:文本分块读取怎样避免截断多字节字符? 使用 Reader 或保持同一个 CharsetDecoder 的状态,不能对每个任意字节块独立构造 String。

八、加强记忆

二进制数据使用字节流,文本使用字符流;字节和字符之间的桥梁是 InputStreamReaderOutputStreamWriter,转换时必须明确字符集。缓冲流不改变数据类型,它的价值是将多次小额操作合并,减少底层 I/O 调用。