字节流
字节流是 Java IO 中最基础、最通用的一类流。计算机中的文件、网络数据、图片、视频、压缩包最终都可以看作字节序列,因此字节流可以处理几乎所有类型的数据。
# 1. 字节流的核心类
字节流的两个顶层抽象类是:
InputStream:字节输入流,从外部读取字节到程序。OutputStream:字节输出流,从程序写出字节到外部。
常用方法如下:
int read()
int read(byte[] b)
int read(byte[] b, int off, int len)
void write(int b)
void write(byte[] b)
void write(byte[] b, int off, int len)
void flush()
void close()
read() 返回 -1 表示已经读取到流的末尾。
# 2. 读取字节文件
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class ReadBytesDemo {
public static void main(String[] args) throws IOException {
try (InputStream input = new FileInputStream("demo.bin")) {
byte[] buffer = new byte[8192];
int len;
while ((len = input.read(buffer)) != -1) {
// 只处理 buffer[0, len) 范围内的数据
System.out.println("read bytes: " + len);
}
}
}
}
注意不要直接处理整个 buffer,因为最后一次读取通常不会填满数组,真实有效长度是 len。
# 3. 写入字节文件
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public class WriteBytesDemo {
public static void main(String[] args) throws IOException {
byte[] data = "hello io".getBytes(StandardCharsets.UTF_8);
try (OutputStream output = new FileOutputStream("demo.txt")) {
output.write(data);
output.flush();
}
}
}
flush() 的作用是把缓冲区中的数据刷新到底层目标。close() 通常会自动调用 flush(),但在需要及时发送数据的场景中仍然会显式调用。
# 4. 文件复制示例
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class CopyFileDemo {
public static void main(String[] args) throws IOException {
try (InputStream input = new FileInputStream("source.zip");
OutputStream output = new FileOutputStream("target.zip")) {
byte[] buffer = new byte[8192];
int len;
while ((len = input.read(buffer)) != -1) {
output.write(buffer, 0, len);
}
}
}
}
这个例子可以复制任意二进制文件,因为它不尝试把字节解释成字符。
# 5. 字节流适合什么场景
适合使用字节流的场景:
- 图片、音频、视频文件处理。
- 压缩包、二进制协议处理。
- HTTP 请求和响应体。
- Socket 网络通信。
- 文件上传下载。
- 加密、摘要、压缩等二进制处理。
不适合只使用字节流直接处理的场景:
- 逐行读取文本。
- 按字符处理中文、emoji 等 Unicode 文本。
- 需要明确字符集的文本解析。
这些场景通常应该使用字符流,或者先用字节流读取后再按指定字符集解码。
# 6. 单字节读取的问题
下面这种写法在小文件中可以工作,但不适合大文件:
int b;
while ((b = input.read()) != -1) {
output.write(b);
}
原因是每次读取一个字节都可能触发底层 IO 调用,系统调用成本远高于普通内存操作。生产代码中通常使用字节数组作为缓冲区。
# 专家实践清单
| 场景 | 推荐写法 | 不推荐写法 |
|---|---|---|
| 复制普通文件 | Files.copy 或缓冲区循环 | 单字节循环读写 |
| 处理大文件 | 固定大小 byte[] 分块 | readAllBytes 一次性读取 |
| 写网络响应 | 按实际读取长度写出 | 忽略 len 写满缓冲区 |
| 资源关闭 | try-with-resources | 依赖 GC 或手动漏关 |
字节流读写的关键流程:
创建 InputStream
│
▼
准备 byte[] buffer
│
▼
循环 read,判断 len != -1
│
▼
按 len 处理有效字节
│
▼
关闭资源
# Tips 快问快答
Q:字节流处理的最小单位是什么?
A:字节,也就是 byte,适合处理原始二进制数据。
Q:read() 返回 -1 表示什么?
A:表示已经读到流末尾,没有更多数据。
Q:为什么写出时要使用 write(buffer, 0, len)?
A:最后一次读取可能没有填满缓冲区,只能写真实读取到的字节数。
Q:字节流能不能处理文本? A:可以,但要自己处理编码;文本场景通常用字符流更清晰。
Q:为什么不要单字节循环读取大文件? A:频繁底层 IO 调用成本高,性能会明显变差。
Q:InputStream.available() 能代表文件大小吗?
A:不能,它只表示当前可无阻塞读取的估计字节数。
Q:字节流需要 flush 吗?
A:输入流不需要;输出流如果有缓冲,关闭前需要刷新或关闭。
Q:生产代码中最常见的字节流错误是什么?
A:忘记判断 read 返回长度、忘记关闭资源、一次性读取大文件。
# 7. 总结
字节流是 Java IO 的底层基础。只要数据不是明确的文本,优先使用字节流。写字节流代码时要重点关注三点:循环读取时判断 -1、写入时使用真实长度、通过 try-with-resources 正确关闭资源。