一、前言
谈到 Java 的文件读写操作,字节流可以说是最基础也最高频的 I/O 场景之一。作为 Java I/O 体系的两大核心分支,字节流专门负责处理所有二进制数据的传输——包括图片、音频、视频以及任何非文本文件。而 FileInputStream 和 FileOutputStream 正是这个体系中最关键的两个实现类。深入理解它们,就等于拿到了 Java 文件 I/O 的入门钥匙,为后续的高阶操作打下坚实基础。

本文将围绕这两个类的构造方法、核心 API、使用方式、底层原理以及常见陷阱展开详细讲解,确保你学完后能够编写出既健壮又高效的文件读写代码,从容应对日常开发中的字节流操作需求。
二、字节流简介
Java 的 I/O 流主要分为两大类:
- 字节流(Byte Stream):以
InputStream/OutputStream为抽象父类,按字节(8 bit)进行读写,适用于所有类型的文件,包括二进制文件。 - 字符流(Character Stream):以
Reader/Writer为抽象父类,按字符(16 bit)进行读写,更适合纯文本文件。
FileInputStream 和 FileOutputStream 分别继承自 InputStream 和 OutputStream,是操作文件最直接、最常用的入口类。
InputStream (抽象)
└── FileInputStream ← 从文件读取字节
OutputStream (抽象)
└── FileOutputStream ← 向文件写入字节
三、核心 API 速览
3.1 FileInputStream
| 方法签名 | 说明 |
|---|---|
FileInputStream(String name) | 根据文件路径创建输入流 |
FileInputStream(File file) | 根据 File 对象创建输入流 |
int read() | 读取一个字节,返回 0~255 的 int,读到末尾返回 -1 |
int read(byte[] b) | 读取最多 b.length 个字节到数组,返回实际读取的字节数 |
int read(byte[] b, int off, int len) | 读取最多 len 个字节到数组的指定偏移位置 |
long skip(long n) | 跳过并丢弃 n 个字节 |
int available() | 返回可读的字节数估计值 |
void close() | 关闭流,释放系统资源 |
3.2 FileOutputStream
| 方法签名 | 说明 |
|---|---|
FileOutputStream(String name) | 根据路径创建输出流(覆盖模式) |
FileOutputStream(String name, boolean append) | 指定是否追加模式 |
FileOutputStream(File file) | 根据 File 对象创建输出流 |
void write(int b) | 写入一个字节(只取 int 的低 8 位) |
void write(byte[] b) | 写入整个字节数组 |
void write(byte[] b, int off, int len) | 写入数组的一部分 |
void flush() | 刷新缓冲区(字节流 flush 是空实现,但遵循约定) |
void close() | 关闭流,释放系统资源 |
四、代码实现
4.1 基础用法:单个字节读写(不推荐用于大文件)
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileInputStreamDemo {
public static void main(String[] args) throws IOException {
// 用 try-with-resources 自动关闭流(JDK 7+)
try (FileInputStream fis = new FileInputStream("source.txt");
FileOutputStream fos = new FileOutputStream("dest.txt")) {
int data;
while ((data = fis.read()) != -1) {
fos.write(data);
}
}
System.out.println("文件复制完成");
}
}
缺点:每次只读写一个字节,每次 read()/write() 都涉及一次 native 调用,处理大文件时性能极差,不推荐用于生产环境。
4.2 推荐用法:字节数组批量读写
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileCopyWithBuffer {
public static void main(String[] args) {
String source = "photo.jpg";
String dest = "photo_copy.jpg";
try (FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192]; // 8KB 缓冲区
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
// 注意:必须使用 write(buffer, 0, len) 而不是 write(buffer)
// 因为最后一次读取可能不足 8192 字节
}
System.out.println("复制完成");
} catch (IOException e) {
System.err.println("文件操作失败: " + e.getMessage());
}
}
}
4.3 追加模式写入
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class FileAppendDemo {
public static void main(String[] args) {
try (FileOutputStream fos = new FileOutputStream("log.txt", true)) {
String line = "[" + System.currentTimeMillis() + "] 用户登录成功\n";
fos.write(line.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
}
FileOutputStream 的第二个参数 append 为 true 时,写入内容会追加到文件末尾而非覆盖原内容,非常适合日志记录等场景。
五、执行流程与底层原理
以 read(byte[]) 为例,一次读取的完整链路如下:
- Java 应用层 调用
FileInputStream.read(byte[]) - JDK 内部 调用
native readBytes方法 - JNI 进入 C 层,调用操作系统的
ReadFile(Windows)或read()(Linux/POSIX) - 内核 从磁盘读取数据到内核空间缓冲区
- 内核 → 用户空间 数据从内核缓冲区拷贝到 Java 传入的 byte 数组
- 返回实际读取的字节数(-1 表示 EOF)
这个过程中涉及一次用户态 ↔ 内核态切换和一次数据拷贝。这也是为什么批量读(减少 native 调用次数)比逐字节读性能高出几个数量级的根本原因。
六、常见问题与注意事项
6.1 流必须关闭
未关闭流会导致文件句柄泄漏,最终可能触发 Too many open files 异常,影响系统稳定性。
✅ 正确做法:优先使用 try-with-resources(JDK 7+),它会自动调用 close() 方法,即使在异常发生时也能正确释放资源。
6.2 write(byte[]) 和 write(byte[], 0, len) 的区别
fos.write(buffer) 会把整个 buffer 写入文件。如果最后一次读取的数据不足 buffer 长度,就会写入上一次残留的脏数据。因此始终使用 write(buffer, 0, len) 来确保只写入实际读取到的字节。
6.4 字符编码问题
字节流本身不涉及编码转换,写入什么字节就原样读出什么字节。如果需要在字节流上处理文本,必须在外部显式指定字符集:
// 写入时指定编码
fos.write("中文".getBytes(StandardCharsets.UTF_8));
// 读取时指定编码
String text = new String(bytes, StandardCharsets.UTF_8);
6.5 FileNotFoundException 不等于文件不存在
FileInputStream 构造时如果文件不存在会抛出 FileNotFoundException。但如果文件是一个目录,或者路径中包含不可读的目录,同样的异常也会抛出。不要简单将它等同于“文件不存在”来处理,应当根据具体异常信息做更精细的错误处理。
七、最佳实践
- 能用字符流的场景(纯文本)优先用字符流,否则用字节流。FileReader / FileWriter 内部封装了字节转换,使用起来更省心。
- 大文件优先使用 BufferedInputStream / BufferedOutputStream 进行包装,它们内部会自动维护更大的缓冲区,进一步减少 native 调用次数,提升性能。
- 版本兼容性无需担心 — FileInputStream 和 FileOutputStream 从 JDK 1.0 就已存在,不存在版本兼容问题。
- Files.copy() 可简化操作 — 对于单纯的文件复制任务,使用
java.nio.file.Files.copy()一行代码即可搞定,底层利用更高效的 FileChannel 实现。
// NIO 一行复制
Files.copy(Path.of("source.txt"), Path.of("dest.txt"), StandardCopyOption.REPLACE_EXISTING);
八、总结
FileInputStream和FileOutputStream是 Java 字节流体系中最基础的文件读写类,支持所有文件类型,是学习 Java I/O 的必知必会内容。- 批量读写(通过字节数组)比逐字节读写性能高出几个数量级,日常开发务必采用批量方式。
- 始终使用
try-with-resources确保流被正确关闭,避免资源泄漏。 - 写入时注意使用
write(buffer, 0, len)而非write(buffer),防止脏数据写入文件。 - 纯文本场景优先考虑
FileReader/FileWriter或 NIO 的Files工具类,操作更简洁。
掌握字节流是深入理解 Java I/O 体系的第一步,也是编写高可靠文件操作代码的必备基础。希望本文能帮助你熟练运用 FileInputStream 和 FileOutputStream,从容应对各类文件读写需求。
推荐标签:
- Java IO
- FileInputStream
- FileOutputStream
- 字节流
- Java 文件操作
