Skip to content

InputStream / OutputStream:字节流的「宪法」

你知道 Java 为什么把 read() 设计成返回 int 而不是 byte 吗?

这不是随便选的。byte 的范围是 -128 到 127,而 int 的 -1 被用作「流结束」的标记。如果 read() 返回 byte,你永远无法区分「读到字节 0xFF」和「流结束了」。

这背后隐藏着整个 IO 体系的设计哲学。

InputStream 的方法签名:三重境界

InputStream 的方法设计,像是在看一本武功秘籍——从最基础的 read() 到高阶的 mark()/reset(),每一层都有它的用途。

第一层:单字节读取

java
public abstract class InputStream implements Closeable {
    // 抽象方法,子类必须实现
    public abstract int read() throws IOException;
}

这是所有读取操作的根基。返回值有三种情况:

返回值含义
0 ~ 255成功读到 1 字节(无符号)
-1流结束了,没有更多数据
java
int b = in.read();
// b = 65  表示读到了字节 0x41(ASCII 'A')
// b = -1  表示流结束了

每次 read() 都可能触发一次系统调用。读取 1MB 数据,如果逐字节读,需要调用 100 万次系统调用。

第二层:批量读取

java
// 批量读取到数组(默认实现效率低,不推荐直接用)
public int read(byte[] b) throws IOException {
    return read(b, 0, b.length);
}

// 批量读取到数组指定区间
public int read(byte[] b, int off, int len) throws IOException {
    // 返回实际读取的字节数,-1 表示流结束
}

批量读取的威力:

java
// ❌ 差:1MB 文件触发 100 万次系统调用
FileInputStream fis = new FileInputStream("big.dat");
int b;
while ((b = fis.read()) != -1) {
    process(b);
}

// ✅ 好:1MB 文件只触发 128 次系统调用
FileInputStream fis = new FileInputStream("big.dat");
byte[] buf = new byte[8192];
int len;
while ((len = fis.read(buf)) != -1) {
    process(buf, len);  // 处理有效数据 buf[0] ~ buf[len-1]
}

核心差异:系统调用的开销是内存操作的数千倍。一次 read(byte[]) 读 8KB,比 8192 次 read() 各读 1 字节快 100 倍以上。

第三层:标记与回退

java
// 判断是否支持 mark/reset
public boolean markSupported();  // FileInputStream 支持,ByteArrayInputStream 支持

// 标记当前位置,后续最多回退 readLimit 字节
public void mark(int readLimit);

// 重置到上次 mark 的位置
public void reset() throws IOException;

这有什么用?想象你需要「偷看」数据流的前几个字节,判断数据类型后再决定怎么处理:

java
BufferedInputStream bis = new BufferedInputStream(
    new FileInputStream("data.bin"));

bis.mark(4);  // 标记,容许回退最多 4 字节

byte[] header = new byte[4];
bis.read(header);  // 读取文件头

if (matchesMagicNumber(header)) {
    bis.reset();  // 是目标文件,从头处理
    processFile(bis);
} else {
    // 不是目标文件,跳过
    // 不需要 reset,因为要跳过这段数据
}

其他辅助方法

java
// 跳过 n 字节(不常用,但有用)
public long skip(long n) throws IOException;

// 返回可无阻塞读取的字节数(估计值)
// 用途:预分配缓冲区大小
public int available() throws IOException;

// 关闭流(释放系统资源)
public void close() throws IOException;

OutputStream:写入的镜像设计

OutputStream 的设计是 InputStream 的镜像,核心方法一一对应:

java
public abstract class OutputStream implements Closeable, Flushable {
    // 单字节写入(抽象方法)
    public abstract void write(int b) throws IOException;

    // 批量写入数组
    public void write(byte[] b) throws IOException {
        write(b, 0, b.length);
    }

    // 批量写入数组区间
    public void write(byte[] b, int off, int len) throws IOException;

    // 刷新缓冲区(强制把数据写出)
    public void flush() throws IOException;

    // 关闭流(会自动 flush)
    public void close() throws IOException;
}

flush() 的真正含义

有些流内部有缓冲区(如 BufferedOutputStream),write() 时数据先到缓冲区,满了才真正写到磁盘。flush() 强制把缓冲区内容立即写出。

java
// 写日志到文件,需要及时 flush
try (BufferedOutputStream bos = new BufferedOutputStream(
        new FileOutputStream("app.log"))) {
    bos.write("error occurred".getBytes());
    bos.flush();  // 不 flush?程序崩溃时这行日志就丢了
}

对于没有缓冲的 FileOutputStreamflush() 基本是空操作——数据直接到内核缓冲区,写磁盘是操作系统的事。

关闭流:必须用 try-with-resources

InputStreamOutputStream 都实现了 Closeable 接口,必须关闭以释放系统资源。

java
// ✅ 标准写法:try-with-resources(JDK 7+)
try (InputStream in = new FileInputStream("data.bin")) {
    byte[] buf = new byte[8192];
    while (in.read(buf) != -1) {
        process(buf);
    }
}
// 自动关闭,异常也会关闭

// ✅ 多层嵌套也能自动关闭
try (
    BufferedInputStream in = new BufferedInputStream(
        new FileInputStream("data.bin"));
    BufferedOutputStream out = new BufferedOutputStream(
        new FileOutputStream("out.bin"))
) {
    byte[] buf = new byte[8192];
    int len;
    while ((len = in.read(buf)) != -1) {
        out.write(buf, 0, len);
    }
}

// ❌ 差:手动关闭,异常时不关闭
OutputStream out = new FileOutputStream("data.bin");
out.write("data".getBytes());
out.close();  // 如果上面抛异常,这行永远不会执行

为什么只关外层就够了?

BufferedInputStream.close() 会递归关闭底层的 FileInputStream。但如果你用的是 FilterInputStream 的子类,确保关闭最外层即可。

方法对照表

读(InputStream)写(OutputStream)说明
read()write(int)单字节操作
read(byte[])write(byte[])批量操作
read(byte[], off, len)write(byte[], off, len)区间操作
skip(n)-跳过字节
available()-估计可读字节
mark()/reset()-标记与回退
close()close()关闭流
-flush()刷新缓冲区

记住这个原则:批量操作比单操作快千倍。这不只是性能优化,而是 IO 编程的基本素养。

基于 VitePress 构建