InputStream / OutputStream:字节流的「宪法」
你知道 Java 为什么把 read() 设计成返回 int 而不是 byte 吗?
这不是随便选的。byte 的范围是 -128 到 127,而 int 的 -1 被用作「流结束」的标记。如果 read() 返回 byte,你永远无法区分「读到字节 0xFF」和「流结束了」。
这背后隐藏着整个 IO 体系的设计哲学。
InputStream 的方法签名:三重境界
看 InputStream 的方法设计,像是在看一本武功秘籍——从最基础的 read() 到高阶的 mark()/reset(),每一层都有它的用途。
第一层:单字节读取
public abstract class InputStream implements Closeable {
// 抽象方法,子类必须实现
public abstract int read() throws IOException;
}这是所有读取操作的根基。返回值有三种情况:
| 返回值 | 含义 |
|---|---|
0 ~ 255 | 成功读到 1 字节(无符号) |
-1 | 流结束了,没有更多数据 |
int b = in.read();
// b = 65 表示读到了字节 0x41(ASCII 'A')
// b = -1 表示流结束了每次 read() 都可能触发一次系统调用。读取 1MB 数据,如果逐字节读,需要调用 100 万次系统调用。
第二层:批量读取
// 批量读取到数组(默认实现效率低,不推荐直接用)
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 表示流结束
}批量读取的威力:
// ❌ 差: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 倍以上。
第三层:标记与回退
// 判断是否支持 mark/reset
public boolean markSupported(); // FileInputStream 支持,ByteArrayInputStream 支持
// 标记当前位置,后续最多回退 readLimit 字节
public void mark(int readLimit);
// 重置到上次 mark 的位置
public void reset() throws IOException;这有什么用?想象你需要「偷看」数据流的前几个字节,判断数据类型后再决定怎么处理:
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,因为要跳过这段数据
}其他辅助方法
// 跳过 n 字节(不常用,但有用)
public long skip(long n) throws IOException;
// 返回可无阻塞读取的字节数(估计值)
// 用途:预分配缓冲区大小
public int available() throws IOException;
// 关闭流(释放系统资源)
public void close() throws IOException;OutputStream:写入的镜像设计
OutputStream 的设计是 InputStream 的镜像,核心方法一一对应:
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() 强制把缓冲区内容立即写出。
// 写日志到文件,需要及时 flush
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("app.log"))) {
bos.write("error occurred".getBytes());
bos.flush(); // 不 flush?程序崩溃时这行日志就丢了
}对于没有缓冲的 FileOutputStream,flush() 基本是空操作——数据直接到内核缓冲区,写磁盘是操作系统的事。
关闭流:必须用 try-with-resources
InputStream 和 OutputStream 都实现了 Closeable 接口,必须关闭以释放系统资源。
// ✅ 标准写法: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 编程的基本素养。
