在 Java 中,文件的读写操作是最基础也是最高频的 I/O 场景之一。字节流(Byte Stream)作为 Java I/O 体系的两大分支之一,负责处理所有二进制数据的传输,图片、音频、视频以及任何非文本文件都离不开它。FileInputStream 和 FileOutputStream 是字节流中最核心的两个实现类,掌握它们就掌握了 Java 文件 I/O 的基石。

本文将围绕这两个类的构造、核心方法、使用方式、底层原理以及常见坑点展开,确保看完后能写出健壮的文件读写代码。
Java 的 I/O 流分为两大类:
InputStream / OutputStream 为抽象父类,按字节(8 bit)读写,适合所有类型的文件。Reader / Writer 为抽象父类,按字符(16 bit)读写,适合纯文本文件。FileInputStream 和 FileOutputStream 分别继承自 InputStream 和 OutputStream,是操作文件的直接入口。
InputStream (抽象)
└── FileInputStream ← 从文件读取字节
OutputStream (抽象)
└── FileOutputStream ← 向文件写入字节
| 方法签名 | 说明 |
|---|---|
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() | 关闭流,释放系统资源 |
| 方法签名 | 说明 |
|---|---|
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() | 关闭流,释放系统资源 |
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 调用,大文件下性能极差。
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());
}
}
}
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[]) 为例,一次读取的完整链路如下:
FileInputStream.read(byte[])native readBytes 方法ReadFile(Windows)或 read()(Linux/POSIX)这个过程中涉及一次用户态 ↔ 内核态切换和一次数据拷贝。这也是为什么批量读(减少 native 调用次数)比逐字节读快几个数量级的原因。
未关闭流会导致文件句柄泄漏,最终可能触发 Too many open files 异常。
✅ 正确做法:优先使用 try-with-resources(JDK 7+),它会自动调用 close(),即使在异常发生时也能正确关闭。
fos.write(buffer) 会把整个 buffer 写入文件。如果最后一次读取不足 buffer 长度,会写入上一次残留的脏数据。永远使用 write(buffer, 0, len)。
字节流不涉及编码转换,写入什么字节就读出什么字节。如果要在字节流上处理文本,必须外部指定字符集:
// 写入时指定编码
fos.write("中文".getBytes(StandardCharsets.UTF_8));
// 读取时指定编码
String text = new String(bytes, StandardCharsets.UTF_8);
FileInputStream 构造时如果文件不存在会抛 FileNotFoundException。但如果文件是个目录,或者路径包含不可读的目录,同一异常也会抛出。不要只把它当成"文件不存在"来处理。
java.nio.file.Files.copy() 一行代码搞定,底层使用更高效的 FileChannel。// NIO 一行复制
Files.copy(Path.of("source.txt"), Path.of("dest.txt"), StandardCopyOption.REPLACE_EXISTING);
FileInputStream 和 FileOutputStream 是 Java 字节流体系中最基础的文件读写类,支持所有文件类型。try-with-resources 确保流被正确关闭。write(buffer, 0, len) 而非 write(buffer),避免脏数据。FileReader / FileWriter 或 NIO 的 Files 工具类。掌握字节流是理解 Java I/O 体系的第一步,也是编写高可靠文件操作代码的必备基础。
推荐标签: