Skip to content

字节流、字符流与缓冲 ​

学习目标 ​

  • 能按字节或字符选择 InputStream、OutputStream、Reader 和 Writer。
  • 会组合缓冲层和显式 Charset,避免逐字节读写与乱码。
  • 能处理 try-with-resources、EOF、mark/reset、压缩流和数据流边界。

核心知识点 ​

专业术语 ​

  • InputStream/OutputStream:以 int 或 byte[] 读写原始字节。
  • Reader/Writer:按字符读写,并通过 Charset 把字节解码或编码。
  • Buffered:用内存缓冲减少底层系统调用,不改变数据语义。
  • EOF(End of File):读取结束信号,字节流通常用 -1 表示。

白话解释与边界 ​

图片、压缩包和协议帧使用字节流;文本才在确定编码后使用字符流。BufferedInputStream 和 BufferedReader 只是加速层,不会把字节自动变成正确字符。所有流都要关闭,写出流尤其要 flush/close,否则末尾缓冲内容可能尚未落盘。

常用用法 ​

InputStream.read:读取一个字节或一批字节 ​

read() 返回的是 0–255 或 -1,不能直接当作有符号 byte;大量数据应使用字节数组重载,避免逐字节调用造成过多系统调用。

java
// 说明:InputStream.read:读取一个字节或一批字节。
import java.io.ByteArrayInputStream;
import java.io.InputStream;

public class InputStreamReadDemo {
    public static void main(String[] args) throws Exception {
        try (InputStream input = new ByteArrayInputStream(new byte[]{65, 66})) {
        // 输入:try (InputStream input = new ByteArrayInputStream(new byte[]{65, 66})) {;资源变量 input 接收 new ByteArrayInputStream(new byte[]{65, 66}),try 结束时自动关闭。
        // 关键变化:try (InputStream input = new ByteArrayInputStream(new byte[]{65, 66})) {;创建资源 input,构造表达式为 new ByteArrayInputStream(new byte[]{65, 66});try 结束时关闭该资源。
            int first = input.read();
            // 关键变化:int first = input.read();;从 input 读取一个字节,返回值写入 first;读到末尾时返回 -1。
            byte[] rest = input.readAllBytes();
            // 关键变化:byte[] rest = input.readAllBytes();;从 input 读取剩余字节,byte[] 返回值写入 rest;读取位置到达末尾。
            System.out.println((char) first + " / " + rest.length);
            // 输出:A / 1
        }
    }
}

OutputStream.write:写出字节 ​

write(int) 只写最低 8 位;写文本前先用明确 Charset 编码。

java
// 说明:OutputStream.write:写出字节。
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

public class OutputStreamWriteDemo {
    public static void main(String[] args) throws Exception {
        try (OutputStream output = new ByteArrayOutputStream()) {
        // 输入:try (OutputStream output = new ByteArrayOutputStream()) {;资源变量 output 接收 new ByteArrayOutputStream(),try 结束时自动关闭。
        // 关键变化:try (OutputStream output = new ByteArrayOutputStream()) {;创建资源 output,构造表达式为 new ByteArrayOutputStream();try 结束时关闭该资源。
            output.write("Java".getBytes(StandardCharsets.UTF_8));
            output.flush();
            // 关键变化:output.flush();;刷新 output,缓冲字节写入目标;缓冲区状态更新。
            System.out.println(output.toString());
            // 输出:Java
        }
    }
}

这里的 ByteArrayOutputStream 不需要真正关闭,但统一使用 try-with-resources 便于替换成文件或网络流。

缓冲字节传输:减少底层读写次数 ​

缓冲流减少底层访问次数;如果底层已经是内存流,性能收益很小,但模式在替换为文件或 Socket 时仍然通用。

java
// 说明:缓冲字节传输:减少底层读写次数。
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;

public class BufferedByteStreamDemo {
    public static void main(String[] args) throws Exception {
        var source = new ByteArrayInputStream(new byte[]{1, 2, 3});
        var target = new ByteArrayOutputStream();
        try (var input = new BufferedInputStream(source);
        // 输入:try (var input = new BufferedInputStream(source);;资源变量 input 接收 new BufferedInputStream(source,try 结束时自动关闭。
        // 关键变化:try (var input = new BufferedInputStream(source);;创建资源 input,构造表达式为 new BufferedInputStream(source;try 结束时关闭该资源。
             var output = new BufferedOutputStream(target)) {
            input.transferTo(output);
            // 关键变化:input.transferTo(output);;从 input 向 output 传输剩余字节,返回传输字节数;读取位置到达末尾。
        }
        System.out.println(target.size());
        // 输出:3
    }
}

InputStream.transferTo:直接转发字节 ​

transferTo 适合完整转发,但不会替调用方关闭输入或输出,也不会限制总字节数;网络输入要配合大小上限和超时。

java
// 说明:InputStream.transferTo:直接转发字节。
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

public class TransferToDemo {
    public static void main(String[] args) throws Exception {
        var input = new ByteArrayInputStream(new byte[]{10, 20});
        var output = new ByteArrayOutputStream();
        long count = input.transferTo(output);
        // 关键变化:long count = input.transferTo(output);;把 input 的剩余字节传给 output,返回实际传输字节数 2 并写入 count。
        System.out.println(count + " / " + output.size());
        // 输出:2 / 2
    }
}

读写字符:使用字符流传输文本 ​

字符流按 UTF-16 char 处理,不等于按 Unicode 码点处理;表情等补充字符可能占用两个 char。

java
// 说明:读写字符:使用字符流传输文本。
import java.io.StringReader;
import java.io.StringWriter;

public class CharacterStreamDemo {
    public static void main(String[] args) throws Exception {
        try (var reader = new StringReader("你好"); var writer = new StringWriter()) {
        // 输入:try (var reader = new StringReader("你好"); var writer = new StringWriter()) {;资源变量 reader 接收 new StringReader("你好"); var writer = new StringWriter(),try 结束时自动关闭。
        // 关键变化:try (var reader = new StringReader("你好"); var writer = new StringWriter()) {;创建资源 reader,构造表达式为 new StringReader("你好"); var writer = new StringWriter();try 结束时关闭该资源。
            writer.write(reader.read());
            char[] rest = new char[1];

            reader.read(rest);
            // 关键变化:reader.read(rest);;从 reader 读取剩余字符“好”到 rest[0],返回字符数 1;读取位置前进。
            writer.write(rest);
            System.out.println(writer);
            // 输出:你好
        }
    }
}

转换字节与字符:显式指定 Charset ​

字节和字符边界必须明确指定同一 Charset;不要依赖 new InputStreamReader(input) 的平台默认编码。

java
// 说明:转换字节与字符:显式指定 Charset。
import java.io.ByteArrayInputStream;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

public class CharsetBridgeDemo {
    public static void main(String[] args) throws Exception {
        byte[] bytes = "你好".getBytes(StandardCharsets.UTF_8);
        try (var reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes), StandardCharsets.UTF_8))) {
        // 输入:try (var reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes), StandardCharsets.UTF_8))) {;资源变量 reader 接收 new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes), StandardCharsets.UTF_8)),try 结束时自动关闭。
        // 关键变化:try (var reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes), StandardCharsets.UTF_8))) {;创建资源 reader,构造表达式为 new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes), StandardCharsets.UTF_8));try 结束时关闭该资源。
            System.out.println(reader.readLine());
            // 输出:你好
        }
    }
}

BufferedReader(Reader):为字符输入增加缓冲 ​

BufferedReader 接收已有的 Reader 并在其上增加缓冲,构造时不读取数据,关闭外层 reader 时会继续关闭传入的 source。

java
// 说明:BufferedReader(Reader):为字符输入增加缓冲。
import java.io.BufferedReader;
import java.io.StringReader;

StringReader source = new StringReader("first\nsecond");
try (BufferedReader reader = new BufferedReader(source)) {
// 输入:try (BufferedReader reader = new BufferedReader(source)) {;资源变量 reader 接收 new BufferedReader(source),try 结束时自动关闭。
// 关键变化:try (BufferedReader reader = new BufferedReader(source)) {;创建资源 reader,构造表达式为 new BufferedReader(source);try 结束时关闭该资源。
    // reader 从 source 成批填充内部缓冲,readLine() 只返回本行字符。
    System.out.println(reader.readLine());
    // 输出:first
}

BufferedReader.readLine:按行读取 ​

readLine 不返回行尾换行符;行很长或输入不可信时仍要考虑内存上限。

java
// 说明:BufferedReader.readLine:按行读取。
import java.io.BufferedReader;
import java.io.StringReader;

public class BufferedReaderDemo {
    public static void main(String[] args) throws Exception {
        try (BufferedReader reader = new BufferedReader(new StringReader("java\nsql"))) {
        // 输入:try (BufferedReader reader = new BufferedReader(new StringReader("java\nsql"))) {;资源变量 reader 接收 new BufferedReader(new StringReader("java\nsql")),try 结束时自动关闭。
        // 关键变化:try (BufferedReader reader = new BufferedReader(new StringReader("java\nsql"))) {;创建资源 reader,构造表达式为 new BufferedReader(new StringReader("java\nsql"));try 结束时关闭该资源。
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
                // 输出:java
            }
        }
    }
}

需要流式组合时可用 reader.lines(),同样要关闭 Reader。

缓冲文本输出:选择异常透明或格式化写入 ​

BufferedWriter 适合受控文本写出;PrintWriter 便于格式化,但默认会吞掉部分 I/O 异常并通过 checkError() 报告,不能误当成异常透明的 Writer。

java
// 说明:缓冲文本输出:选择异常透明或格式化写入。
import java.io.BufferedWriter;
import java.io.StringWriter;
import java.io.PrintWriter;

public class TextWriterDemo {
    public static void main(String[] args) throws Exception {
        var target = new StringWriter();
        try (var writer = new BufferedWriter(target)) {
        // 输入:try (var writer = new BufferedWriter(target)) {;资源变量 writer 接收 new BufferedWriter(target),try 结束时自动关闭。
        // 关键变化:try (var writer = new BufferedWriter(target)) {;创建资源 writer,构造表达式为 new BufferedWriter(target);try 结束时关闭该资源。
            writer.write("java");
            writer.newLine();
            // 关键变化:writer.newLine();;调用 newLine,实参为 无显式参数;向 writer 写入平台换行符;文本内容追加换行。
            writer.write("api");
        }
        try (var print = new PrintWriter(target)) {
        // 输入:try (var print = new PrintWriter(target)) {;资源变量 print 接收 new PrintWriter(target),try 结束时自动关闭。
        // 关键变化:try (var print = new PrintWriter(target)) {;创建资源 print,构造表达式为 new PrintWriter(target);try 结束时关闭该资源。
            print.println("done");
            // 关键变化:print.println("done");;向 print 写入 "done" 并换行;输出流新增一行。
        }
        System.out.println(target.toString().replace(System.lineSeparator(), "|"));
        // 输出:java|api|done|
    }
}

Charset:显式编码与解码 ​

优先使用 StandardCharsets 常量,避免拼写错误和不必要的受检异常;协议、文件格式和数据库连接都应记录字符集约定。

java
// 说明:Charset:显式编码与解码。
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

public class CharsetDemo {
    public static void main(String[] args) {
        Charset charset = StandardCharsets.UTF_8;

        byte[] bytes = "Java".getBytes(charset);
        System.out.println(new String(bytes, charset));
        // 输出:Java
    }
}

不常用但需要知道 ​

回看输入:标记位置后复位 ​

是否支持 mark、回看范围多大由具体流决定;不确定时使用 BufferedInputStream 或自己缓存数据,不能假定所有网络流都可 reset。

java
import java.io.ByteArrayInputStream;

public class MarkResetDemo {
    public static void main(String[] args) throws Exception {
        try (var input = new ByteArrayInputStream(new byte[]{1, 2, 3})) {
        // 输入:try (var input = new ByteArrayInputStream(new byte[]{1, 2, 3})) {;资源变量 input 接收 new ByteArrayInputStream(new byte[]{1, 2, 3}),try 结束时自动关闭。
        // 关键变化:try (var input = new ByteArrayInputStream(new byte[]{1, 2, 3})) {;创建资源 input,构造表达式为 new ByteArrayInputStream(new byte[]{1, 2, 3});try 结束时关闭该资源。
            System.out.println(input.read());
            // 输出:1
            input.mark(2);
            // 关键变化:input.mark(2);;调用 mark,实参为 2;记录 input 当前 position 作为 mark;后续 reset 可回到该位置。
            System.out.println(input.read() + ", " + input.read());
            // 输出:2, 3
            input.reset();
            // 关键变化:input.reset();;调用 reset,实参为 无显式参数;将 input 的 position 恢复到最近 mark。
            System.out.println(input.read());
            // 输出:2
        }
    }
}

读写基本类型:使用配对二进制端点 ​

Data 流是 Java 定义的二进制格式,不是通用 JSON 或跨语言协议;读写顺序、类型和端序必须由双方固定。

java
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;

public class DataStreamDemo {
    public static void main(String[] args) throws Exception {
        var bytes = new ByteArrayOutputStream();
        try (var out = new DataOutputStream(bytes)) {
        // 输入:try (var out = new DataOutputStream(bytes)) {;资源变量 out 接收 new DataOutputStream(bytes),try 结束时自动关闭。
        // 关键变化:try (var out = new DataOutputStream(bytes)) {;创建资源 out,构造表达式为 new DataOutputStream(bytes);try 结束时关闭该资源。
            out.writeInt(20);
            // 关键变化:out.writeInt(20);;调用 writeInt,实参为 20;向 out 写入整数 20;数据流追加 4 字节。
            out.writeUTF("java");
            // 关键变化:out.writeUTF("java");;调用 writeUTF,实参为 "java";向 out 写入 UTF 字符串 "java";数据流追加带长度的文本。
        }
        try (var in = new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
        // 输入:try (var in = new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {;资源变量 in 接收 new DataInputStream(new ByteArrayInputStream(bytes.toByteArray())),try 结束时自动关闭。
        // 关键变化:try (var in = new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {;创建资源 in,构造表达式为 new DataInputStream(new ByteArrayInputStream(bytes.toByteArray()));try 结束时关闭该资源。
            System.out.println(in.readInt() + " / " + in.readUTF());
            // 输出:20 / java
        }
    }
}

压缩字节流:使用 GZIP 输入输出端点 ​

压缩流改变的是传输格式,不是安全加密;解压不可信输入要限制展开大小,避免压缩炸弹。

java
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

public class GzipStreamDemo {
    public static void main(String[] args) throws Exception {
        var compressed = new ByteArrayOutputStream();
        try (var out = new GZIPOutputStream(compressed)) {
        // 输入:try (var out = new GZIPOutputStream(compressed)) {;资源变量 out 接收 new GZIPOutputStream(compressed),try 结束时自动关闭。
        // 关键变化:try (var out = new GZIPOutputStream(compressed)) {;创建资源 out,构造表达式为 new GZIPOutputStream(compressed);try 结束时关闭该资源。
            out.write("java".getBytes());
        }
        try (var in = new GZIPInputStream(new ByteArrayInputStream(compressed.toByteArray()))) {
        // 输入:try (var in = new GZIPInputStream(new ByteArrayInputStream(compressed.toByteArray()))) {;资源变量 in 接收 new GZIPInputStream(new ByteArrayInputStream(compressed.toByteArray())),try 结束时自动关闭。
        // 关键变化:try (var in = new GZIPInputStream(new ByteArrayInputStream(compressed.toByteArray()))) {;创建资源 in,构造表达式为 new GZIPInputStream(new ByteArrayInputStream(compressed.toByteArray()));try 结束时关闭该资源。
            System.out.println(new String(in.readAllBytes()));
            // 输出:java
        }
    }
}

SequenceInputStream:串联多个输入流 ​

它适合兼容需要一个 InputStream 的旧 API;大量片段或可组合的文件处理优先考虑 NIO 流和 Channel。

java
import java.io.ByteArrayInputStream;
import java.io.SequenceInputStream;

public class SequenceStreamDemo {
    public static void main(String[] args) throws Exception {
        try (var input = new SequenceInputStream(
                new ByteArrayInputStream(new byte[]{1}),
                new ByteArrayInputStream(new byte[]{2}))) {
                // 输入:try (var input = new SequenceInputStream( new ByteArrayInputStream(new byte[]{1}), new ByteArrayInputStream(new byte[]{2}))) {;资源变量 input 接收 new SequenceInputStream( new ByteArrayInputStream(new byte[]{1}), new ByteArrayInputStream(new byte[]{2})),try 结束时自动关闭。
                // 关键变化:try (var input = new SequenceInputStream( new ByteArrayInputStream(new byte[]{1}), new ByteArrayInputStream(new byte[]{2}))) {;创建资源 input,构造表达式为 new SequenceInputStream( new ByteArrayInputStream(new byte[]{1}), new ByteArrayInputStream(new byte[]{2}));try 结束时关闭该资源。
            System.out.println(input.read() + ", " + input.read());
            // 输出:1, 2
        }
    }
}

PushbackInputStream:把字节推回输入 ​

推回缓冲区容量有限,适合词法分析或协议前缀探测;复杂回看不要无限 unread。

java
import java.io.ByteArrayInputStream;
import java.io.PushbackInputStream;

public class PushbackStreamDemo {
    public static void main(String[] args) throws Exception {
        try (var input = new PushbackInputStream(new ByteArrayInputStream(new byte[]{'A', 'B'}))) {
        // 输入:try (var input = new PushbackInputStream(new ByteArrayInputStream(new byte[]{'A', 'B'}))) {;资源变量 input 接收 new PushbackInputStream(new ByteArrayInputStream(new byte[]{'A', 'B'})),try 结束时自动关闭。
        // 关键变化:try (var input = new PushbackInputStream(new ByteArrayInputStream(new byte[]{'A', 'B'}))) {;创建资源 input,构造表达式为 new PushbackInputStream(new ByteArrayInputStream(new byte[]{'A', 'B'}));try 结束时关闭该资源。
            int first = input.read();
            // 关键变化:int first = input.read();;从 input 读取一个字节,返回值写入 first;读到末尾时返回 -1。
            input.unread(first);
            // 关键变化:input.unread(first);;调用 unread,实参为 first;把 first 放回 input 的输入前端;下次 read 先返回该值。
            System.out.println((char) input.read());
            // 输出:A
        }
    }
}

简单案例 ​

ByteArrayInputStream:从内存字节创建输入流 ​

它适合把已有字节数组交给只接受 InputStream 的 API。

java
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
byte[] data = "OK".getBytes(StandardCharsets.UTF_8);
try (var in = new ByteArrayInputStream(data)) {
// 输入:try (var in = new ByteArrayInputStream(data)) {;资源变量 in 接收 new ByteArrayInputStream(data),try 结束时自动关闭。
// 关键变化:try (var in = new ByteArrayInputStream(data)) {;创建资源 in,构造表达式为 new ByteArrayInputStream(data);try 结束时关闭该资源。
    System.out.println(in.read());
    // 输出:79
    System.out.println(in.available());
    // 输出:1
}

ByteArrayOutputStream:在内存中累计输出字节 ​

它适合体积可控的数据;toByteArray 返回独立副本。

java
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
try (var out = new ByteArrayOutputStream()) {
// 输入:try (var out = new ByteArrayOutputStream()) {;资源变量 out 接收 new ByteArrayOutputStream(),try 结束时自动关闭。
// 关键变化:try (var out = new ByteArrayOutputStream()) {;创建资源 out,构造表达式为 new ByteArrayOutputStream();try 结束时关闭该资源。
    out.write("OK".getBytes(StandardCharsets.UTF_8));
    System.out.println(out.size());
    // 输出:2
    System.out.println(out.toString(StandardCharsets.UTF_8));
    // 输出:OK
}

InputStreamReader:按字符集把字节转成字符 ​

构造时显式传入字符集,避免依赖不同机器的平台默认编码。

java
import java.io.*;
import java.nio.charset.StandardCharsets;
var bytes = new ByteArrayInputStream("中文".getBytes(StandardCharsets.UTF_8));
try (var reader = new InputStreamReader(bytes, StandardCharsets.UTF_8)) {
// 输入:try (var reader = new InputStreamReader(bytes, StandardCharsets.UTF_8)) {;资源变量 reader 接收 new InputStreamReader(bytes, StandardCharsets.UTF_8),try 结束时自动关闭。
// 关键变化:try (var reader = new InputStreamReader(bytes, StandardCharsets.UTF_8)) {;创建资源 reader,构造表达式为 new InputStreamReader(bytes, StandardCharsets.UTF_8);try 结束时关闭该资源。
    System.out.println((char) reader.read());
    // 输出:首字符“中”
    System.out.println(reader.getEncoding());
    // 输出:编码为 UTF8
}

File.exists:检查路径是否存在 ​

检查与后续操作之间可能发生竞态;能直接执行目标操作时不要把它当权限保证。

java
import java.io.File;
File current = new File(".");
System.out.println(current.exists());
// 输出:true
System.out.println(current.isDirectory());
// 输出:current.isDirectory() = false
System.out.println(new File(current, "missing.file").exists());
// 输出:missing.file.exists() = false

FileOutputStream:打开文件字节输出 ​

构造后必须可靠关闭;第二个参数为 true 时才是追加写入。

java
import java.io.FileOutputStream;
import java.nio.file.Files;
var path = Files.createTempFile("note-", ".txt");
try (var out = new FileOutputStream(path.toFile())) { out.write(65); }
System.out.println(Files.readString(path));
// 输出:A
Files.delete(path);
// 关键变化:Files.delete(path);;删除存在的路径 path,返回 void;路径不存在时抛出 NoSuchFileException。

BufferedReader.close:关闭缓冲读取链 ​

关闭最外层读取器会向下关闭其包装的底层 Reader,优先交给 try-with-resources。

java
import java.io.*;
var source = new StringReader("line");
var reader = new BufferedReader(source);
System.out.println(reader.readLine());
// 输出:line
reader.close();
// 关键变化:reader.close();;关闭 reader,释放底层资源;后续读写进入关闭状态。
try { reader.readLine(); }
// 关键变化:try { reader.readLine(); };从 reader 读取下一行并返回文本,EOF 时返回 null;读取位置向前推进。
catch (IOException e) { System.out.println("closed"); }
// 输出:closed

File.transferTo 不存在:文件传输使用 InputStream.transferTo ​

java.io.File 没有 transferTo;打开输入、输出流后才能复制字节。

java
// 说明:transferTo 把 in 中的 0x41、0x42 两个字节复制到 out;copied 记录字节数,toString() 用平台默认编码解码为 AB。
import java.io.*;
var in = new ByteArrayInputStream(new byte[] {65, 66});
var out = new ByteArrayOutputStream();
long copied = in.transferTo(out);
// 关键变化:long copied = in.transferTo(out);;把 in 中的 2 个字节传给 out,返回传输字节数 2 并写入 copied。
System.out.println(copied);
// 输出:2
System.out.println(out.toString());
// 输出:AB

IOException:构造带原因的 I/O 异常 ​

封装底层失败时保留 cause,让上层仍能追踪根因。

java
import java.io.IOException;
var cause = new IllegalStateException("disk unavailable");
var error = new IOException("save failed", cause);
System.out.println(error.getMessage());
// 输出:save failed
System.out.println(error.getCause().getMessage());
// 输出:disk unavailable

PrintWriter:创建格式化字符输出器 ​

它适合格式化文本,但写入错误默认不抛出,必要时调用 checkError。

java
import java.io.*;
var buffer = new StringWriter();
try (var writer = new PrintWriter(buffer)) {
// 输入:try (var writer = new PrintWriter(buffer)) {;资源变量 writer 接收 new PrintWriter(buffer),try 结束时自动关闭。
// 关键变化:try (var writer = new PrintWriter(buffer)) {;创建资源 writer,构造表达式为 new PrintWriter(buffer);try 结束时关闭该资源。
    writer.printf("id=%d", 7);
}
System.out.println(buffer);
// 输出:id=7
System.out.println(buffer.toString().length());
// 输出:4

StringWriter:创建内存字符输出器 ​

它常用于接收只支持 Writer 的 API 输出,不需要底层文件。

java
import java.io.StringWriter;
var writer = new StringWriter();
writer.write("hello");
writer.append('!');
// 关键变化:writer.append('!');;向 StringWriter 追加字符 '!',返回 writer 自身;文本内容变为 hello!。
System.out.println(writer.getBuffer().length());
// 输出:6
System.out.println(writer.toString());
// 输出:hello!

StringWriter.toString:取得累计文本 ​

调用会返回当前缓冲区内容,之后仍可继续向 writer 写入。

java
import java.io.StringWriter;
var writer = new StringWriter();
writer.write("A");
String first = writer.toString();
writer.write("B");
System.out.println(first);
// 输出:A
System.out.println(writer.toString());
// 输出:AB

Paths.get:把文本片段组合为 Path ​

它不创建真实文件,只按当前文件系统规则构造路径对象。

java
import java.nio.file.Paths;
var path = Paths.get("logs", "app.log");
// 关键变化:var path = Paths.get("logs", "app.log");;把路径片段 "logs" 和 "app.log" 组合为 Path 并写入 path;路径对象创建完成。
System.out.println(path.getFileName());
// 输出:app.log
System.out.println(path.getNameCount());
// 输出:2
System.out.println(path.isAbsolute());
// 输出:false

ZipOutputStream:创建 ZIP 压缩输出流 ​

每个条目要先 putNextEntry,写完后关闭条目与整个流。

java
import java.io.*;
import java.util.zip.*;
var bytes = new ByteArrayOutputStream();
try (var zip = new ZipOutputStream(bytes)) {
// 输入:try (var zip = new ZipOutputStream(bytes)) {;资源变量 zip 接收 new ZipOutputStream(bytes),try 结束时自动关闭。
// 关键变化:try (var zip = new ZipOutputStream(bytes)) {;创建资源 zip,构造表达式为 new ZipOutputStream(bytes);try 结束时关闭该资源。
    zip.putNextEntry(new ZipEntry("a.txt"));
    // 关键变化:zip.putNextEntry(new ZipEntry("a.txt"));;调用 putNextEntry,实参为 new ZipEntry("a.txt");向 zip 打开 new ZipEntry("a.txt");后续写入进入该 ZIP 条目。
    zip.write(65); zip.closeEntry();
}
System.out.println(bytes.size() > 0);
// 输出:true
java
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.StringReader;
import java.io.StringWriter;

public class StreamDemo {
    public static void main(String[] args) throws Exception {
        StringWriter storage = new StringWriter();
        try (var writer = new BufferedWriter(storage)) {
        // 输入:try (var writer = new BufferedWriter(storage)) {;资源变量 writer 接收 new BufferedWriter(storage),try 结束时自动关闭。
        // 关键变化:try (var writer = new BufferedWriter(storage)) {;创建资源 writer,构造表达式为 new BufferedWriter(storage);try 结束时关闭该资源。
            writer.write("Java");
            writer.newLine();
            // 关键变化:writer.newLine();;调用 newLine,实参为 无显式参数;向 writer 写入平台换行符;文本内容追加换行。
            writer.write("I/O");
        }
        try (var reader = new BufferedReader(new StringReader(storage.toString()))) {
        // 输入:try (var reader = new BufferedReader(new StringReader(storage.toString()))) {;资源变量 reader 接收 new BufferedReader(new StringReader(storage.toString())),try 结束时自动关闭。
        // 关键变化:try (var reader = new BufferedReader(new StringReader(storage.toString()))) {;创建资源 reader,构造表达式为 new BufferedReader(new StringReader(storage.toString()));try 结束时关闭该资源。
            System.out.println(reader.readLine());
            // 输出:Java
            System.out.println(reader.readLine());
            // 输出:I/O
        }
    }
}

案例展示了字符、缓冲、行和资源关闭的组合;改成文件时再加 Files.newBufferedReader/newBufferedWriter 和显式 Charset。

易混点 ​

  • InputStream.read() 的 -1 是 EOF,不能把 byte 的负值当成结束。
  • 缓冲流提高访问效率,但不改变字符集,也不自动解决半包或消息边界。
  • Reader 按 char 读,不等于按 Unicode 码点读;需要码点时使用 codePoints()。
  • PrintWriter 方便但可能吞异常,文件关键写入不要忽略 checkError 或底层 IOException。

课后小问 ​

  1. 为什么 InputStreamReader 必须传入 Charset? 答案:它负责把字节解码成字符,缺少明确编码就会依赖平台默认值。 解析:文件和网络协议应显式约定 UTF-8 等编码,读写两侧必须一致。

  2. 为什么写完 BufferedWriter 还要关闭或 flush? 答案:数据可能仍在内存缓冲中,没有 flush/close 就未必真正写到下层。 解析:try-with-resources 既会 flush 又会关闭资源,并能在异常路径执行清理。

本节小结 ​

  • 字节流处理原始数据,字符流处理编码后的文本。
  • 缓冲层减少系统调用,不能替代 Charset、协议边界或错误处理。
  • readLine、transferTo、压缩流和数据流各有明确格式与资源边界。
  • 所有流都要关闭,写出流还要确认缓冲内容已提交。

快速回顾 ​

  • 二进制用 InputStream/OutputStream,文本用 Reader/Writer。
  • StandardCharsets.UTF_8 是明确编码的常用选择。
  • read() 返回 -1 表示 EOF,批量读写优先使用数组或 transferTo。
  • mark/reset、DataStream、GZIP 和 PrintWriter 都有额外语义与限制。