Java字节流FileInputStream和FileOutputStream从入门到实践
作者:一团柳絮
一、前言
在 Java 中,文件的读写操作是最基础也是最高频的 I/O 场景之一。字节流(Byte Stream)作为 Java I/O 体系的两大分支之一,负责处理所有二进制数据的传输,图片、音频、视频以及任何非文本文件都离不开它。FileInputStream 和 FileOutputStream 是字节流中最核心的两个实现类,掌握它们就掌握了 Java 文件 I/O 的基石。
本文将围绕这两个类的构造、核心方法、使用方式、底层原理以及常见坑点展开,确保看完后能写出健壮的文件读写代码。
二、字节流简介
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.2write(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 字节流体系中最基础的文件读写类,支持所有文件类型。- 批量读写(字节数组)比逐字节读写性能高出几个数量级。
- 始终使用
try-with-resources确保流被正确关闭。 - 写入时注意
write(buffer, 0, len)而非write(buffer),避免脏数据。 - 纯文本场景优先考虑
FileReader/FileWriter或 NIO 的Files工具类。
掌握字节流是理解 Java I/O 体系的第一步,也是编写高可靠文件操作代码的必备基础。
推荐标签:
- Java IO
- FileInputStream
- FileOutputStream
- 字节流
- Java 文件操作
到此这篇关于Java字节流FileInputStream和FileOutputStream从入门到实践的文章就介绍到这了,更多相关Java字节流FileInputStream和FileOutputStream内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
