字节流
本教程共 100 篇 · 第 86 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
本节目标:掌握 InputStream 和 OutputStream 的核心用法,能用字节流完成任意文件的读取与复制。
字节流处理的是原始数据
字节流是 Java I/O 的地基。所有其他流,包括字符流,底层都建立在字节流之上。
它搬运的单位是字节,不做任何解释。一张 JPG 图片、一个 ZIP 压缩包、一段 MP4 视频,在字节流眼里都只是一长串 0 到 255 的数字。
两个抽象基类分工明确:InputStream 负责读,OutputStream 负责写。
InputStream 的 read 方法
InputStream 最核心的方法只有一个:
public abstract int read() throws IOException;
这个方法读一个字节,然后返回。
新手第一个疑问通常是:既然读的是字节,为什么返回类型是 int 而不是 byte?
原因在于要留一个值表示「读完了」。Java 的 byte 是有符号的,取值范围 -128 到 127,8 位全被占满,挤不出一个特殊值。
换成 int 后,正常读到的字节被提升为 0 到 255 的正数,剩下的负数区间就空出来了。Java 选用 -1 表示流已到末尾。
Warning千万别写
byte b = input.read();。这样一来-1会被截断成(byte) -1,也就是0xFF,跟正常读到的 255 撞车,循环永远停不下来。
逐字节读取文件
把上面的规则写成循环,就是最基础的读文件代码:
import java.io.*;
public class ReadBytes {
public static void main(String[] args) throws IOException {
// 先造一个文件,保证示例可以独立运行
try (OutputStream out = new FileOutputStream("bytes.txt")) {
out.write(new byte[] {72, 101, 108, 108, 111}); // Hello
}
try (InputStream input = new FileInputStream("bytes.txt")) {
int n;
while ((n = input.read()) != -1) { // 读到 -1 就结束
System.out.print((char) n);
}
}
System.out.println();
}
}
运行:
javac ReadBytes.java
java ReadBytes
输出 Hello。
while ((n = input.read()) != -1) 这个写法把「读取」和「判断」合并成一行,是 Java I/O 的固定套路,看多了就习惯了。
用缓冲区批量读取
逐字节读虽然能跑,但慢得离谱。每调一次 read(),底层就可能触发一次系统调用,开销远大于搬运一个字节本身。
InputStream 提供了两个重载方法来一次读一批:
int read(byte[] b):尽量填满数组b,返回实际读到的字节数int read(byte[] b, int off, int len):指定填充的起始位置和最大长度
注意返回值含义变了:不再是字节的值,而是这次读了多少个字节。返回 -1 仍然表示到达末尾。
import java.io.*;
public class BufferedRead {
public static void main(String[] args) throws IOException {
try (OutputStream out = new FileOutputStream("big.dat")) {
for (int i = 0; i < 5000; i++) {
out.write(i % 256);
}
}
try (InputStream input = new FileInputStream("big.dat")) {
byte[] buffer = new byte[1024]; // 1KB 缓冲区
int n;
int total = 0;
while ((n = input.read(buffer)) != -1) {
total += n; // n 是本次实际读到的字节数
}
System.out.println("共读取 " + total + " 字节");
}
}
}
输出:
共读取 5000 字节
Tip缓冲区大小取 1024、4096、8192 这类 2 的幂比较常见,跟操作系统的页大小对齐,效率好。太小起不到作用,太大浪费内存。
OutputStream 的写入
OutputStream 的核心方法是 write(),同样有三个版本:
write(int b):写一个字节,只取参数的低 8 位write(byte[] b):写整个数组write(byte[] b, int off, int len):写数组的一部分
还有一个 flush() 方法值得单独说。
出于效率考虑,很多输出流会先把数据攒在内存里,攒够一批再真正写出去。flush() 的作用是「别攒了,现在就写」。
close() 内部会自动调用 flush(),所以正常关闭的流不会丢数据。但如果流要长期开着(比如网络连接),就需要在关键时刻手动 flush()。
追加写入
FileOutputStream 默认会清空原文件重新写。想在末尾追加,构造时传入第二个参数 true:
import java.io.*;
public class AppendDemo {
public static void main(String[] args) throws IOException {
try (OutputStream out = new FileOutputStream("log.txt")) {
out.write("第一行\n".getBytes("UTF-8"));
}
// 第二个参数 true 表示追加,不覆盖
try (OutputStream out = new FileOutputStream("log.txt", true)) {
out.write("第二行\n".getBytes("UTF-8"));
}
try (InputStream in = new FileInputStream("log.txt")) {
System.out.println(new String(in.readAllBytes(), "UTF-8"));
}
}
}
输出:
第一行
第二行
readAllBytes() 是 Java 9 加进来的便捷方法,一次性读完整个流。方便,但只适合小文件。
常用实现类
FileInputStream 和 FileOutputStream 连接硬盘文件,是用得最多的一对。
ByteArrayInputStream 和 ByteArrayOutputStream 连接内存中的 byte 数组。写单元测试时很好用——不用真的建文件,就能构造出一个流:
import java.io.*;
public class MemoryStream {
public static void main(String[] args) throws IOException {
byte[] data = {74, 97, 118, 97}; // Java
try (InputStream input = new ByteArrayInputStream(data)) {
int n;
while ((n = input.read()) != -1) {
System.out.print((char) n);
}
}
System.out.println();
}
}
这里体现了一个重要原则:方法参数尽量声明成 InputStream 而不是 FileInputStream。这样同一个方法既能处理文件,也能处理内存数据和网络数据。
复制文件的标准写法
把读和写拼起来,就是文件复制:
import java.io.*;
public class CopyFile {
public static void main(String[] args) throws IOException {
try (OutputStream seed = new FileOutputStream("source.bin")) {
seed.write("待复制的内容".getBytes("UTF-8"));
}
try (InputStream in = new FileInputStream("source.bin");
OutputStream out = new FileOutputStream("target.bin")) {
byte[] buffer = new byte[4096];
int n;
while ((n = in.read(buffer)) != -1) {
out.write(buffer, 0, n); // 必须传 n,不能写整个数组
}
}
System.out.println("复制完成");
}
}
Warning
out.write(buffer, 0, n)里的n不能省。最后一次读取往往填不满缓冲区,直接写整个数组会把上一轮的残留数据也写进去,文件末尾就多出一段垃圾。这是复制文件时最经典的 bug。
try-with-resources 括号里可以用分号声明多个资源,关闭顺序与声明顺序相反。
跳过与探测:skip 和 available
除了读和写,InputStream 还有两个偶尔会用到的方法。
long skip(long n) 表示跳过 n 个字节不读。比如某种文件格式的前 128 字节是文件头,你只关心正文,就可以先跳过去。它的返回值是实际跳过的字节数,可能比要求的少,因为流可能提前结束了。
int available() 返回当前不阻塞就能读到的字节数。对本地文件来说,这个值通常等于剩余长度;但对网络流来说,它只反映此刻网卡缓冲区里的数据量,跟对方总共要发多少毫无关系。
Warning很多教程会写
byte[] buf = new byte[in.available()]一次性读完,这个写法在文件上碰巧能跑通,换成网络流就会读到半截数据。养成用循环读的习惯,别依赖available()判断长度。
字节流适合什么、不适合什么
判断标准其实很简单:看你关不关心内容的「含义」。
处理图片、音频、视频、压缩包、可执行文件这类二进制数据,字节流是唯一正确的选择。这些数据本来就没有「字符」的概念,逐字节原样搬运才不会损坏。
做文件复制、上传下载、加密解密时也一样。中间环节根本不需要理解内容,转成字符反而多此一举,还有编码出错的风险。
反过来,如果要读一个 UTF-8 的配置文件、按行处理日志、拼接字符串输出,那就该用下一节的字符流。用字节流硬读中文,很容易在缓冲区边界把一个汉字劈成两半,拼出乱码。
Note一个汉字在 UTF-8 下占 3 个字节。假设缓冲区是 1024 字节,某次刚好读到某个汉字的第 2 个字节就截断了,把这一批转成字符串必然出现问号。字符流之所以存在,就是为了替你处理这类边界问题。
本节小结
read()返回int,-1表示流结束,绝不能用byte接收。- 逐字节读效率极低,实际开发一律用
byte[]缓冲区批量读。 - 批量读时返回值是本次读到的字节数,写出时必须带上这个长度。
FileOutputStream第二个参数true表示追加写入。close()会自动flush(),长期开着的流才需要手动刷新。- 方法参数用抽象类型
InputStream,让代码适配所有数据源。