
文章目录
-
- [引言:Java IO体系与文件操作](#引言:Java IO体系与文件操作)
- 第一章:IO流基础概念
-
- [1.1 什么是流(Stream)](#1.1 什么是流(Stream))
- [1.2 Java IO流的分类](#1.2 Java IO流的分类)
- [1.3 文件IO的核心类](#1.3 文件IO的核心类)
- 第二章:File类深度剖析
-
- [2.1 类的定义与核心字段](#2.1 类的定义与核心字段)
- [2.2 构造方法:创建File对象](#2.2 构造方法:创建File对象)
- [2.3 常用API详解](#2.3 常用API详解)
-
- [2.3.1 获取文件和目录基本信息](#2.3.1 获取文件和目录基本信息)
- [2.3.2 判断功能](#2.3.2 判断功能)
- [2.3.3 列出目录内容](#2.3.3 列出目录内容)
- [2.3.4 创建与删除](#2.3.4 创建与删除)
- [2.3.5 重命名与移动](#2.3.5 重命名与移动)
- [2.4 应用场景与最佳实践](#2.4 应用场景与最佳实践)
- [2.5 File类的局限性](#2.5 File类的局限性)
- 第三章:FileInputStream源码剖析与使用
-
- [3.1 类定义与继承体系](#3.1 类定义与继承体系)
- [3.2 核心字段与构造方法](#3.2 核心字段与构造方法)
-
- [3.2.1 核心字段](#3.2.1 核心字段)
- [3.2.2 构造方法](#3.2.2 构造方法)
- [3.3 核心方法详解](#3.3 核心方法详解)
-
- [3.3.1 read():读取单个字节](#3.3.1 read():读取单个字节)
- [3.3.2 read(byte[] b):读取到字节数组](#3.3.2 read(byte[] b):读取到字节数组)
- [3.3.3 skip(long n):跳过字节](#3.3.3 skip(long n):跳过字节)
- [3.3.4 available():可用字节数估计](#3.3.4 available():可用字节数估计)
- [3.3.5 close():关闭流](#3.3.5 close():关闭流)
- [3.3.6 getChannel():获取文件通道](#3.3.6 getChannel():获取文件通道)
- [3.4 线程安全性分析](#3.4 线程安全性分析)
- [3.5 使用示例与最佳实践](#3.5 使用示例与最佳实践)
- [3.6 性能优化建议](#3.6 性能优化建议)
- 第四章:FileOutputStream源码剖析与使用
-
- [4.1 类定义与继承体系](#4.1 类定义与继承体系)
- [4.2 核心字段与构造方法](#4.2 核心字段与构造方法)
-
- [4.2.1 核心字段](#4.2.1 核心字段)
- [4.2.2 构造方法](#4.2.2 构造方法)
- [4.3 核心方法详解](#4.3 核心方法详解)
-
- [4.3.1 write(int b):写入单个字节](#4.3.1 write(int b):写入单个字节)
- [4.3.2 write(byte[] b):写入字节数组](#4.3.2 write(byte[] b):写入字节数组)
- [4.3.3 close():关闭流](#4.3.3 close():关闭流)
- [4.3.4 getChannel()和getFD()](#4.3.4 getChannel()和getFD())
- [4.4 使用示例与最佳实践](#4.4 使用示例与最佳实践)
-
- 示例1:基本文件写入
- [示例2:追加模式 vs 覆盖模式](#示例2:追加模式 vs 覆盖模式)
- 示例3:写入二进制数据
- [4.5 注意事项与常见陷阱](#4.5 注意事项与常见陷阱)
- 第五章:综合实战与最佳实践
-
- [5.1 文件复制工具完整实现](#5.1 文件复制工具完整实现)
- [5.2 文件加密/解密示例(异或算法)](#5.2 文件加密/解密示例(异或算法))
- [5.3 配置文件读取示例](#5.3 配置文件读取示例)
- [5.4 资源管理最佳实践:try-with-resources](#5.4 资源管理最佳实践:try-with-resources)
- [5.5 常见问题与解决方案](#5.5 常见问题与解决方案)
- 第六章:总结与展望
-
- [6.1 三大核心类对比](#6.1 三大核心类对比)
- [6.2 从字节流到高级IO的演进](#6.2 从字节流到高级IO的演进)
- [6.3 未来趋势](#6.3 未来趋势)
- [6.4 给开发者的建议](#6.4 给开发者的建议)
- 附录:常用代码片段速查

引言:Java IO体系与文件操作
在Java应用程序开发中,文件输入输出(I/O)是最基础且最常见的操作之一。无论是读取配置文件、处理用户上传的文档、记录日志信息,还是进行数据持久化,都离不开对文件的操作。Java的I/O体系通过"流"(Stream)的抽象,为开发者提供了一套统一而强大的API来处理各种设备间的数据传递。
本文将深入剖析Java文件IO的三大基石:
- File类:文件和目录路径名的抽象表示,用于文件和目录的创建、删除、查询等操作
- FileInputStream:字节文件输入流,用于从文件中读取原始字节数据
- FileOutputStream:字节文件输出流,用于将原始字节数据写入文件
我们将从源码层面解读其设计原理,探讨核心方法的使用技巧,分析性能优化的关键点,并提供最佳实践指南。全文预计超过8000字,力求让你彻底掌握Java文件IO的精髓。
第一章:IO流基础概念
1.1 什么是流(Stream)
在Java中,流是一个抽象的概念,代表了数据的"流动"。可以将其想象为连接数据源(源端)和程序(目的端)的一条管道,数据在管道中按顺序传输。
输入流(Input Stream):数据从外部源(如文件、网络、键盘)流入程序(内存)。程序从输入流中读取数据。
输出流(Output Stream):数据从程序(内存)流向外部目的地(如文件、网络、屏幕)。程序向输出流中写入数据。
1.2 Java IO流的分类
Java的IO流体系可以从三个维度进行分类:
| 分类维度 | 类别 | 说明 | 典型类 |
|---|---|---|---|
| 数据流向 | 输入流 | 读取数据到程序 | InputStream, Reader |
| 输出流 | 从程序写出数据 | OutputStream, Writer |
|
| 操作单位 | 字节流 | 以字节(8位)为单位 | InputStream, OutputStream |
| 字符流 | 以字符(16位)为单位 | Reader, Writer |
|
| 角色分工 | 节点流 | 直接从数据源读写 | FileInputStream, FileOutputStream |
| 处理流 | 包装节点流,提供增强功能 | BufferedInputStream, DataInputStream |
字节流 vs 字符流:
- 字节流:处理所有类型的文件(文本、图片、视频、音频等)。因为所有文件在底层都是以字节形式存储的,字节流是通用的。
- 字符流:专门处理文本文件。它内部处理字符编码和解码,方便处理人类可读的文本。
节点流 vs 处理流:
- 节点流:直接连接数据源,是IO操作的基础
- 处理流:在节点流之上进行包装,提供缓冲、转换、对象序列化等高级功能(装饰器模式)
1.3 文件IO的核心类
本文聚焦于文件IO的基础字节流:
- File:代表文件或目录路径,提供文件系统操作(创建、删除、重命名、查询属性等)
- FileInputStream:字节文件输入流,从文件中读取字节数据
- FileOutputStream:字节文件输出流,向文件中写入字节数据
这三者构成了Java进行原始文件操作的基础。理解它们的工作原理,是掌握更高级IO(如缓冲流、对象流、NIO)的前提。
第二章:File类深度剖析
java.io.File类是Java IO体系中唯一代表文件和目录路径名的类,但它不负责文件内容的读写。它更像是一个文件或目录的"名片",记录了路径信息,并提供了对文件元数据(属性)的操作方法。
2.1 类的定义与核心字段
java
public class File implements Serializable, Comparable<File> {
// 文件系统相关的操作接口(与具体操作系统交互)
private static final FileSystem fs = DefaultFileSystem.getFileSystem();
// 核心字段:存储文件的路径
private final String path;
// 路径的规范化状态
private transient volatile PathStatus status = null;
// 文件路径的规范化字符串
private volatile transient String prefixLength;
// 构造方法
public File(String pathname) {
if (pathname == null) {
throw new NullPointerException();
}
this.path = fs.normalize(pathname);
}
// ...
}
关键分析:
File类实现了Serializable接口,意味着File对象可以被序列化- 实现了
Comparable<File>接口,提供了compareTo方法,可以按路径名字典序比较 - 核心字段
path被final修饰,说明File对象是不可变的------一旦创建,其代表的抽象路径名就不能改变 FileSystem fs是与底层操作系统交互的关键,所有与文件系统相关的操作(如检查文件是否存在、获取文件属性)最终都委托给它
2.2 构造方法:创建File对象
File类提供了多种构造方法,灵活适应不同的使用场景:
java
public class FileConstructorDemo {
public static void main(String[] args) {
// 1. 通过完整路径名字符串创建
File file1 = new File("D:\\data\\document.txt"); // Windows路径需转义
File file2 = new File("/home/user/document.txt"); // Linux/Mac路径
// 2. 通过父路径和子路径字符串创建
String parent = "D:\\data";
String child = "document.txt";
File file3 = new File(parent, child);
// 3. 通过父File对象和子路径字符串创建
File parentDir = new File("D:\\data");
File file4 = new File(parentDir, "document.txt");
// 4. 通过URI创建(略)
}
}
重要说明:
-
路径分隔符 :Windows使用反斜杠
\,在Java字符串中需要转义为\\;Unix/Linux/Mac使用正斜杠/。更好的做法是使用File.separator常量,它会根据运行平台自动适配:javaFile file = new File("D:" + File.separator + "data" + File.separator + "document.txt"); -
创建时机 :
new File()只是在内存中创建了一个对象,代表一个路径,并不会在硬盘上实际创建文件或目录 。文件是否真正存在,需要通过exists()方法验证。 -
相对路径 :相对路径相对于当前工作目录(可通过
System.getProperty("user.dir")获取)。在IDEA中,单元测试方法的相对路径相对于当前module,main方法的相对路径相对于当前工程。
2.3 常用API详解
File类的API主要分为四大类:获取基本信息、判断功能、列出目录内容、创建删除操作。
2.3.1 获取文件和目录基本信息
java
import java.io.File;
import java.util.Date;
public class FileGetInfoDemo {
public static void main(String[] args) {
File file = new File("test.txt");
System.out.println("文件名: " + file.getName()); // test.txt
System.out.println("路径: " + file.getPath()); // test.txt(构造时的路径)
System.out.println("绝对路径: " + file.getAbsolutePath()); // 完整绝对路径
try {
System.out.println("规范路径: " + file.getCanonicalPath()); // 解析.和..的绝对路径
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("父目录: " + file.getParent()); // null(如果没有父目录)
System.out.println("文件大小: " + file.length() + " 字节"); // 文件实际大小
System.out.println("最后修改时间: " + new Date(file.lastModified())); // 毫秒值
}
}
关键点:
length()返回0的情况:文件不存在,或者文件确实为空lastModified()返回的是从1970-01-01 UTC开始的毫秒数getAbsolutePath()与getCanonicalPath()的区别:绝对路径可能包含.或..,规范路径会解析这些相对引用
2.3.2 判断功能
java
import java.io.File;
public class FileCheckDemo {
public static void main(String[] args) {
File file = new File("test.txt");
System.out.println("是否存在: " + file.exists()); // true/false
System.out.println("是否是文件: " + file.isFile()); // true
System.out.println("是否是目录: " + file.isDirectory()); // false
System.out.println("是否可读: " + file.canRead()); // 权限检查
System.out.println("是否可写: " + file.canWrite()); // 权限检查
System.out.println("是否可执行: " + file.canExecute()); // 对于文件是可执行权限,对于目录是可遍历权限
System.out.println("是否隐藏: " + file.isHidden()); // 平台相关
}
}
注意 :isFile()和isDirectory()在文件不存在时都返回false,不是抛出异常。
2.3.3 列出目录内容
java
import java.io.File;
public class FileListDemo {
public static void main(String[] args) {
File dir = new File("D:\\data");
if (dir.exists() && dir.isDirectory()) {
// 方法1:返回字符串数组(仅名称)
String[] fileNames = dir.list();
System.out.println("目录中的文件和子目录:");
for (String name : fileNames) {
System.out.println(" " + name);
}
// 方法2:返回File对象数组(完整路径)
File[] files = dir.listFiles();
System.out.println("\nFile对象列表:");
for (File f : files) {
System.out.println(" " + f.getPath() + (f.isDirectory() ? " [目录]" : " [文件]"));
}
// 带文件名过滤器的版本
File[] txtFiles = dir.listFiles((d, name) -> name.endsWith(".txt"));
System.out.println("\n文本文件:");
for (File f : txtFiles) {
System.out.println(" " + f.getName());
}
}
}
}
源码分析 :listFiles()最终会调用FileSystem的list()方法,这是一个native方法,与操作系统交互获取目录内容。
2.3.4 创建与删除
java
import java.io.File;
import java.io.IOException;
public class FileCreateDeleteDemo {
public static void main(String[] args) {
// 1. 创建文件
File file = new File("newfile.txt");
try {
if (file.createNewFile()) {
System.out.println("文件创建成功: " + file.getName());
} else {
System.out.println("文件已存在,无需创建");
}
} catch (IOException e) {
System.out.println("创建文件时发生IO错误");
e.printStackTrace();
}
// 2. 创建单级目录
File singleDir = new File("mydir");
if (singleDir.mkdir()) {
System.out.println("目录创建成功: " + singleDir.getName());
} else {
System.out.println("目录创建失败(可能已存在或父目录不存在)");
}
// 3. 创建多级目录
File multiDir = new File("parent/child/grandchild");
if (multiDir.mkdirs()) {
System.out.println("多级目录创建成功: " + multiDir.getPath());
} else {
System.out.println("多级目录创建失败");
}
// 4. 删除文件或空目录
if (file.delete()) {
System.out.println("文件删除成功");
} else {
System.out.println("文件删除失败(可能不存在或无权限)");
}
// 5. 程序退出时自动删除
File tempFile = new File("temp.txt");
try {
if (tempFile.createNewFile()) {
tempFile.deleteOnExit(); // JVM退出时删除
System.out.println("临时文件将在程序退出时删除");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
重要说明:
createNewFile():原子操作,检查文件是否存在并创建,返回boolean表示是否成功创建mkdir()vsmkdirs():前者要求父目录必须存在,后者会创建所有不存在的父目录delete():直接删除,不走回收站,需谨慎操作deleteOnExit():注册一个钩子,在JVM正常退出时删除文件,适用于临时文件清理
2.3.5 重命名与移动
java
import java.io.File;
public class FileRenameDemo {
public static void main(String[] args) {
File oldFile = new File("oldname.txt");
File newFile = new File("newname.txt");
// 确保原文件存在
try {
oldFile.createNewFile();
} catch (Exception e) {
e.printStackTrace();
}
// 重命名/移动
if (oldFile.renameTo(newFile)) {
System.out.println("重命名成功");
System.out.println("新文件存在: " + newFile.exists());
System.out.println("旧文件存在: " + oldFile.exists());
} else {
System.out.println("重命名失败");
}
}
}
关键点:
renameTo(File dest)的行为依赖于平台- 在同一文件系统内,相当于重命名(原子操作)
- 在不同文件系统间,可能表现为复制+删除,不是原子操作
- 目标文件不能已存在(某些平台会覆盖)
2.4 应用场景与最佳实践
场景1:递归遍历目录树
java
import java.io.File;
public class DirectoryTraversal {
public static void traverse(File dir, String indent) {
if (!dir.exists() || !dir.isDirectory()) {
System.out.println(indent + dir.getPath() + " (不是有效目录)");
return;
}
File[] files = dir.listFiles();
if (files == null) return;
for (File file : files) {
if (file.isDirectory()) {
System.out.println(indent + "[DIR] " + file.getName());
traverse(file, indent + " "); // 递归
} else {
System.out.println(indent + "[FILE] " + file.getName() + " (" + file.length() + " 字节)");
}
}
}
public static void main(String[] args) {
File root = new File("D:\\data");
traverse(root, "");
}
}
场景2:文件过滤器实现
java
import java.io.File;
import java.io.FileFilter;
import java.io.FilenameFilter;
public class FileFilterDemo {
public static void main(String[] args) {
File dir = new File("D:\\data");
// 使用FilenameFilter(过滤文件名)
String[] images = dir.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".jpg") || name.endsWith(".png");
}
});
// 使用FileFilter(过滤File对象)
File[] largeFiles = dir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isFile() && pathname.length() > 1024 * 1024; // > 1MB
}
});
// Lambda简化版
File[] hiddenFiles = dir.listFiles(f -> f.isHidden());
}
}
2.5 File类的局限性
尽管File类是文件操作的基础,但它存在一些局限性:
- 不能访问文件内容:File类只操作元数据,不能读写文件内容
- 操作失败处理:很多方法只返回boolean,不提供详细的失败原因
- 符号链接处理:对符号链接的支持有限
- 文件属性:无法设置文件所有者、权限等高级属性
- 大文件支持:length()返回long,理论上支持大文件,但一些方法如lastModified()精度有限
JDK 7引入了java.nio.file.Path和Files类,提供了更强大的文件操作功能,但在基础学习中,File类仍然是入门文件IO的第一步。
第三章:FileInputStream源码剖析与使用
java.io.FileInputStream是字节文件输入流,用于从文件中读取原始字节数据。它是InputStream抽象类的直接子类,适用于读取二进制文件(如图片、音频、视频)或任何需要按字节处理的文件。
3.1 类定义与继承体系
java
public class FileInputStream extends InputStream
继承关系:
java.lang.Object
└── java.io.InputStream
└── java.io.FileInputStream
FileInputStream继承了InputStream,因此它拥有所有输入流的基本方法:read()、read(byte[])、close()等,并根据文件读取的特性进行了实现。
3.2 核心字段与构造方法
3.2.1 核心字段
java
public class FileInputStream extends InputStream {
/* 文件描述符,用于打开文件的句柄 */
private final FileDescriptor fd;
/* 引用文件的路径,如果流是通过文件描述符创建时该值为null */
private final String path;
/* 文件通道,用于NIO操作 */
private FileChannel channel = null;
/* 关闭锁,确保close操作的线程安全性 */
private final Object closeLock = new Object();
/* 标记流是否已关闭 */
private volatile boolean closed = false;
// ... 其他代码
}
字段解读:
- FileDescriptor fd:文件描述符,是操作系统用于管理打开文件的句柄。它包含了打开文件的关键信息,后续所有读写操作都通过它进行。
- String path :记录文件的路径,用于错误信息和跟踪。如果流是通过已有的
FileDescriptor创建的,则此字段为null。 - FileChannel channel :NIO中的通道,提供了与文件关联的通道,可以进行内存映射、文件锁定等高级操作。它是懒加载的,只有在调用
getChannel()时才会创建。 - closeLock :用于同步
close()方法,防止多个线程同时关闭导致的问题。 - closed :
volatile修饰,确保多线程间的可见性。
3.2.2 构造方法
FileInputStream提供了三种重载的构造方法:
java
// 1. 通过文件路径名字符串创建
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
// 2. 通过File对象创建(最常用)
public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name); // 安全检查:是否有读取权限
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
fd = new FileDescriptor();
fd.attach(this); // 将当前流关联到文件描述符
path = name;
open(name); // 调用native方法打开文件
}
// 3. 通过已有的FileDescriptor创建
public FileInputStream(FileDescriptor fdObj) {
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkRead(fdObj);
}
fd = fdObj;
path = null;
fd.attach(this); // 将当前流关联到已存在的文件描述符
}
构造过程分析:
- 参数校验和权限检查(
SecurityManager) - 创建(或使用已有的)
FileDescriptor对象 - 调用
attach(this)将文件描述符与当前流关联,便于后续资源释放 - 如果是通过文件路径创建,调用
open(name)本地方法,真正与操作系统交互打开文件 - 如果文件不存在、是目录或无法打开,抛出
FileNotFoundException
核心native方法:
java
private native void open(String name) throws FileNotFoundException;
这个native方法会调用操作系统的API(如Windows的CreateFile,Linux的open)来打开文件,获取文件句柄存储在fd中。
3.3 核心方法详解
3.3.1 read():读取单个字节
java
public int read() throws IOException {
return read0();
}
private native int read0() throws IOException;
工作原理:
- 每次调用读取一个字节(8位)
- 返回值范围:0到255(无符号字节)
- 如果到达文件末尾,返回-1
- 该方法会阻塞,直到有数据可读、到达文件末尾或发生异常
- 底层通过native方法直接调用操作系统读文件的系统调用
性能考量:每次读取一个字节意味着每个字节都要进行一次系统调用,对于大文件来说效率极低。实际开发中几乎不使用此方法读取大量数据。
3.3.2 read(byte[] b):读取到字节数组
java
public int read(byte b[]) throws IOException {
return readBytes(b, 0, b.length);
}
public int read(byte b[], int off, int len) throws IOException {
return readBytes(b, off, len);
}
private native int readBytes(byte b[], int off, int len) throws IOException;
工作原理:
- 尝试读取最多
b.length个字节到数组中 - 返回实际读取的字节数,可能小于请求的长度
- 返回-1表示文件末尾
- native方法
readBytes会尽可能多地读取数据,但受限于文件剩余字节数 - 可以指定偏移量
off,将数据存入数组的指定位置
示例代码:
java
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamReadDemo {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("test.dat")) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
// 处理读取到的数据,注意只处理bytesRead个字节
// buffer中可能只有部分数据是有效的
processData(buffer, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void processData(byte[] data, int length) {
// 处理前length个字节
System.out.println("读取到 " + length + " 字节");
}
}
重要提示 :fis.read(buffer)返回的是实际读取的字节数,这个数值可能小于buffer的长度(特别是在接近文件末尾时)。处理数据时必须使用返回的长度,而不是buffer.length。
3.3.3 skip(long n):跳过字节
java
public native long skip(long n) throws IOException;
工作原理:
- 跳过并丢弃输入流中的n个字节
- 返回实际跳过的字节数(可能小于n)
- 如果n为负数,某些平台支持回退(如例子中
skip(-1)) - 跳过文件末尾不会抛出异常,但后续read会返回-1
示例:
java
FileInputStream fis = new FileInputStream("data.bin");
fis.skip(10); // 跳过前10个字节
int b = fis.read(); // 读取第11个字节
3.3.4 available():可用字节数估计
java
public native int available() throws IOException;
工作原理:
- 返回估计的剩余可读取字节数(不受阻塞)
- 这是一个估计值,不保证精确
- 通常用于判断是否需要创建缓冲区,但不能依赖它作为文件总长度的准确值
- 文件超过EOF时返回0
常见误用:
java
// 错误:用available()确定文件大小
FileInputStream fis = new FileInputStream("file.txt");
byte[] data = new byte[fis.available()]; // 可能太小,也可能太大
fis.read(data); // 可能无法填满缓冲区
正确用法:仅用于非阻塞场景的提示,不能替代循环读取。
3.3.5 close():关闭流
java
public void close() throws IOException {
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}
if (channel != null) {
channel.close();
}
// 关闭文件描述符
fd.closeAll(new Closeable() {
public void close() throws IOException {
close0();
}
});
}
private native void close0() throws IOException;
设计要点:
- 使用
closeLock保证线程安全,防止重复关闭 - 关闭时,如果有关联的
FileChannel,一并关闭 - 通过
fd.closeAll()最终调用native方法close0()释放系统资源 - 推荐使用try-with-resources确保自动关闭
3.3.6 getChannel():获取文件通道
java
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, true, false, this);
}
return channel;
}
}
作用 :返回与此文件输入流关联的唯一的FileChannel对象。这是Java NIO的入口,可以进行更高效的文件操作(如内存映射文件、文件锁定等)。
3.4 线程安全性分析
FileInputStream的实例方法本身不是线程安全的,但它的某些操作具有原子性。
通过多线程测试可以发现:单次read操作本身不会被其他线程抢占而中断,它会完整地读取这次要读取的内容 。但是,由于其他线程可以改变输入流的位置(通过skip或read),每个线程读取时开始的位置是不可预知的。
java
// 来自并发编程网的测试代码
public class FileThreadTest implements Runnable {
private int type;// 0做skip操作,1做读取操作
private int gap;
private FileInputStream in;
// ... 构造方法
@Override
public void run() {
byte[] body = new byte[gap];
if (this.type == 0) {
// 执行skip操作
} else {
// 执行read操作
try {
for(int i = 0; i < 10; i++) {
in.read(body);
System.out.println(Thread.currentThread().getName() + "-" + new String(body));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
测试结果表明:
- 每个
read()调用是原子的,会读取完整的一组字节 - 但由于流的位置是共享的,多个线程交替执行会导致读取位置混乱
- 如果需要线程安全,必须在外部进行同步,或每个线程使用自己的流实例
3.5 使用示例与最佳实践
示例1:基本文件读取(try-with-resources)
java
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamExample {
public static void main(String[] args) {
// JDK 7+ try-with-resources 自动关闭流
try (FileInputStream fis = new FileInputStream("input.dat")) {
byte[] buffer = new byte[4096]; // 4KB缓冲区
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
// 处理数据
System.out.println("读取了 " + bytesRead + " 字节");
// 例如:将字节写入其他流、解析数据等
}
} catch (IOException e) {
System.err.println("文件读取错误: " + e.getMessage());
}
}
}
示例2:复制文件(结合FileOutputStream)
java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileCopyExample {
public static void copyFile(String source, String dest) throws IOException {
try (FileInputStream fis = new FileInputStream(source);
FileOutputStream fos = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192]; // 8KB缓冲区
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
} // 自动关闭两个流
}
public static void main(String[] args) {
try {
copyFile("source.jpg", "copy.jpg");
System.out.println("文件复制成功");
} catch (IOException e) {
System.err.println("复制失败: " + e.getMessage());
}
}
}
示例3:读取部分数据(指定偏移量)
java
import java.io.FileInputStream;
import java.io.IOException;
public class ReadPartialExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("data.bin")) {
byte[] header = new byte[10]; // 读取文件头10字节
byte[] body = new byte[100]; // 读取接下来的100字节
int headerRead = fis.read(header);
if (headerRead == 10) {
System.out.println("文件头读取成功");
}
int bodyRead = fis.read(body);
System.out.println("读取了 " + bodyRead + " 字节的正文");
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.6 性能优化建议
-
使用缓冲流 :
FileInputStream每次读取都会触发系统调用。包装为BufferedInputStream可大幅减少系统调用次数:javatry (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("large.dat"))) { // 读取效率更高 } -
合理设置缓冲区大小:通常4KB-64KB之间,具体取决于应用场景
-
避免在循环中使用单字节读取 :
while ((b = fis.read()) != -1)是性能杀手 -
考虑使用NIO :对于大文件或需要高吞吐量的场景,
FileChannel和内存映射文件性能更好
第四章:FileOutputStream源码剖析与使用
java.io.FileOutputStream是字节文件输出流,用于将原始字节数据写入文件。它是OutputStream抽象类的直接子类,与FileInputStream对应。
4.1 类定义与继承体系
java
public class FileOutputStream extends OutputStream
继承关系:
java.lang.Object
└── java.io.OutputStream
└── java.io.FileOutputStream
4.2 核心字段与构造方法
4.2.1 核心字段
java
public class FileOutputStream extends OutputStream {
/* 文件描述符 */
private final FileDescriptor fd;
/* 文件路径 */
private final String path;
/* 是否以追加模式打开 */
private final boolean append;
/* 文件通道 */
private FileChannel channel;
/* 关闭锁 */
private final Object closeLock = new Object();
/* 是否已关闭 */
private volatile boolean closed = false;
// ...
}
与FileInputStream不同的字段:
- boolean append :标记是否为追加模式。
true表示写入的数据追加到文件末尾,false表示覆盖文件开头。
4.2.2 构造方法
FileOutputStream提供了5个重载的构造方法:
java
// 1. 通过文件名创建(覆盖模式)
public FileOutputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null, false);
}
// 2. 通过文件名创建,指定是否追加
public FileOutputStream(String name, boolean append) throws FileNotFoundException {
this(name != null ? new File(name) : null, append);
}
// 3. 通过File对象创建(覆盖模式)
public FileOutputStream(File file) throws FileNotFoundException {
this(file, false);
}
// 4. 通过File对象创建,指定是否追加
public FileOutputStream(File file, boolean append) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkWrite(name); // 安全检查:是否有写入权限
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
this.fd = new FileDescriptor();
this.append = append;
this.path = name;
// 根据追加模式打开文件
open(name, append);
}
// 5. 通过FileDescriptor创建
public FileOutputStream(FileDescriptor fdObj) {
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkWrite(fdObj);
}
this.fd = fdObj;
this.path = null;
this.append = false;
fd.attach(this);
}
核心native方法:
java
private native void open(String name, boolean append) throws FileNotFoundException;
打开模式说明:
append = false(默认):文件指针定位到文件开头。如果文件已存在,原有内容会被新写入的内容覆盖append = true:文件指针定位到文件末尾,新写入的内容追加到原内容之后
4.3 核心方法详解
4.3.1 write(int b):写入单个字节
java
public void write(int b) throws IOException {
write(b, append);
}
private native void write(int b, boolean append) throws IOException;
工作原理:
- 写入一个字节(参数b的低8位,高24位被忽略)
- 如果流以追加模式打开,写入位置在文件末尾
- 否则在文件开头
- native方法直接调用操作系统写文件系统调用
性能考量 :与FileInputStream.read()类似,单字节写入效率极低,不推荐大量使用。
4.3.2 write(byte[] b):写入字节数组
java
public void write(byte b[]) throws IOException {
writeBytes(b, 0, b.length, append);
}
public void write(byte b[], int off, int len) throws IOException {
writeBytes(b, off, len, append);
}
private native void writeBytes(byte b[], int off, int len, boolean append) throws IOException;
工作原理:
- 将字节数组中的全部或部分数据写入文件
- 建议使用批量写入提高性能
- native方法一次性写入多个字节,减少系统调用次数
4.3.3 close():关闭流
java
public void close() throws IOException {
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}
if (channel != null) {
channel.close();
}
fd.closeAll(new Closeable() {
public void close() throws IOException {
close0();
}
});
}
private native void close0() throws IOException;
与FileInputStream类似:使用锁保证线程安全,关闭关联的通道,释放系统资源。
4.3.4 getChannel()和getFD()
java
public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, false, true, append, this);
}
return channel;
}
}
public final FileDescriptor getFD() throws IOException {
if (fd != null) return fd;
throw new IOException();
}
4.4 使用示例与最佳实践
示例1:基本文件写入
java
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamBasicExample {
public static void main(String[] args) {
String data = "Hello, Java IO!";
try (FileOutputStream fos = new FileOutputStream("output.txt")) {
// 将字符串转换为字节数组写入
fos.write(data.getBytes());
// 也可以逐字节写入(不推荐)
// for (byte b : data.getBytes()) {
// fos.write(b);
// }
System.out.println("数据写入成功");
} catch (IOException e) {
System.err.println("写入失败: " + e.getMessage());
}
}
}
示例2:追加模式 vs 覆盖模式
java
import java.io.FileOutputStream;
import java.io.IOException;
public class AppendVsOverwrite {
public static void main(String[] args) {
// 覆盖模式
try (FileOutputStream fos = new FileOutputStream("test.txt")) {
fos.write("First line\n".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
// 再次写入(覆盖模式)
try (FileOutputStream fos = new FileOutputStream("test.txt")) {
fos.write("Second line\n".getBytes()); // 原内容被覆盖
} catch (IOException e) {
e.printStackTrace();
}
// 追加模式
try (FileOutputStream fos = new FileOutputStream("test.txt", true)) {
fos.write("Third line\n".getBytes()); // 追加到末尾
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行后,test.txt内容为:
Second line
Third line
示例3:写入二进制数据
java
import java.io.FileOutputStream;
import java.io.IOException;
public class WriteBinaryExample {
public static void main(String[] args) {
try (FileOutputStream fos = new FileOutputStream("binary.dat")) {
// 写入int类型(4字节)
int value = 12345678;
fos.write((value >>> 24) & 0xFF); // 高位字节
fos.write((value >>> 16) & 0xFF);
fos.write((value >>> 8) & 0xFF);
fos.write(value & 0xFF); // 低位字节
// 或者使用DataOutputStream简化
// try (DataOutputStream dos = new DataOutputStream(fos)) {
// dos.writeInt(value);
// }
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.5 注意事项与常见陷阱
-
自动创建文件 :如果输出文件不存在,
FileOutputStream会自动创建它(前提是父目录存在) -
目录 vs 文件 :如果指定的路径是一个已存在的目录,会抛出
FileNotFoundException -
权限问题 :如果没有写入权限,也会抛出
FileNotFoundException -
覆盖 vs 追加 :默认为覆盖模式,需要追加时务必使用带
boolean append参数的构造方法 -
数据持久性 :
write()方法返回时,数据不一定已经持久化到磁盘,可能还在操作系统缓存中。需要确保数据真正写入可调用getFD().sync() -
多线程写入 :与
FileInputStream类似,多线程共享同一个FileOutputStream会导致数据交错,需要外部同步或使用每个线程独立的流
第五章:综合实战与最佳实践
5.1 文件复制工具完整实现
java
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
/**
* 文件复制工具 - 展示多种实现方式
*/
public class FileCopyUtil {
/**
* 方式1:使用FileInputStream/FileOutputStream(基础字节流)
*/
public static void copyByStream(String src, String dest) throws IOException {
File srcFile = new File(src);
File destFile = new File(dest);
// 检查源文件是否存在
if (!srcFile.exists()) {
throw new FileNotFoundException("源文件不存在: " + src);
}
// 确保目标文件父目录存在
File parent = destFile.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
// 使用try-with-resources自动关闭资源
try (FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(destFile)) {
byte[] buffer = new byte[8192]; // 8KB缓冲区
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
fos.flush(); // 确保所有数据写入
}
}
/**
* 方式2:使用BufferedInputStream/BufferedOutputStream(缓冲流优化)
*/
public static void copyByBufferedStream(String src, String dest) throws IOException {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dest))) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
}
bos.flush();
}
}
/**
* 方式3:使用FileChannel(NIO,适合大文件)
*/
public static void copyByChannel(String src, String dest) throws IOException {
try (FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest);
FileChannel inChannel = fis.getChannel();
FileChannel outChannel = fos.getChannel()) {
// 直接传输,无需手动缓冲区
inChannel.transferTo(0, inChannel.size(), outChannel);
}
}
/**
* 方式4:使用Files工具类(Java 7+,最简单)
*/
public static void copyByFiles(String src, String dest) throws IOException {
Path sourcePath = Paths.get(src);
Path destPath = Paths.get(dest);
Files.copy(sourcePath, destPath, StandardCopyOption.REPLACE_EXISTING);
}
/**
* 性能测试
*/
public static void performanceTest(String src, String dest) {
long start, end;
try {
// 测试基础流
start = System.currentTimeMillis();
copyByStream(src, dest + ".stream");
end = System.currentTimeMillis();
System.out.println("基础流耗时: " + (end - start) + "ms");
// 测试缓冲流
start = System.currentTimeMillis();
copyByBufferedStream(src, dest + ".buffered");
end = System.currentTimeMillis();
System.out.println("缓冲流耗时: " + (end - start) + "ms");
// 测试NIO通道
start = System.currentTimeMillis();
copyByChannel(src, dest + ".channel");
end = System.currentTimeMillis();
System.out.println("NIO通道耗时: " + (end - start) + "ms");
// 测试Files工具类
start = System.currentTimeMillis();
copyByFiles(src, dest + ".files");
end = System.currentTimeMillis();
System.out.println("Files工具类耗时: " + (end - start) + "ms");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
if (args.length < 2) {
System.out.println("用法: java FileCopyUtil <源文件> <目标文件>");
return;
}
try {
copyByStream(args[0], args[1]);
System.out.println("文件复制成功");
} catch (IOException e) {
System.err.println("复制失败: " + e.getMessage());
}
}
}
5.2 文件加密/解密示例(异或算法)
java
import java.io.*;
/**
* 简单的文件加密/解密工具(使用异或算法)
* 相同的程序执行两次即可解密(异或的特性:a XOR key XOR key = a)
*/
public class FileCipher {
private static final byte DEFAULT_KEY = 0x7F; // 加密密钥
/**
* 加密/解密文件
*/
public static void processFile(String src, String dest, byte key) throws IOException {
try (FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
// 对每个字节进行异或运算
for (int i = 0; i < bytesRead; i++) {
buffer[i] ^= key;
}
fos.write(buffer, 0, bytesRead);
}
}
}
public static void main(String[] args) {
if (args.length < 2) {
System.out.println("用法: java FileCipher <源文件> <目标文件> [密钥]");
return;
}
String src = args[0];
String dest = args[1];
byte key = args.length > 2 ? Byte.parseByte(args[2]) : DEFAULT_KEY;
try {
processFile(src, dest, key);
System.out.println("文件处理完成");
} catch (IOException e) {
System.err.println("处理失败: " + e.getMessage());
}
}
}
5.3 配置文件读取示例
java
import java.io.*;
import java.util.Properties;
/**
* 读取配置文件示例
*/
public class ConfigReader {
private Properties props = new Properties();
/**
* 从文件加载配置
*/
public void loadConfig(String configFile) throws IOException {
try (FileInputStream fis = new FileInputStream(configFile)) {
props.load(fis); // Properties提供了load(InputStream)方法
}
}
/**
* 保存配置到文件
*/
public void saveConfig(String configFile) throws IOException {
try (FileOutputStream fos = new FileOutputStream(configFile)) {
props.store(fos, "Configuration File");
}
}
/**
* 手动解析配置文件(演示FileInputStream用法)
*/
public void manualParseConfig(String configFile) throws IOException {
try (FileInputStream fis = new FileInputStream(configFile)) {
byte[] buffer = new byte[1024];
int bytesRead = fis.read(buffer);
if (bytesRead > 0) {
String content = new String(buffer, 0, bytesRead);
String[] lines = content.split("\n");
for (String line : lines) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue; // 忽略空行和注释
}
int eqIndex = line.indexOf('=');
if (eqIndex > 0) {
String key = line.substring(0, eqIndex).trim();
String value = line.substring(eqIndex + 1).trim();
props.setProperty(key, value);
}
}
}
}
}
public String getProperty(String key) {
return props.getProperty(key);
}
public static void main(String[] args) {
ConfigReader reader = new ConfigReader();
try {
reader.loadConfig("config.properties");
System.out.println("db.url = " + reader.getProperty("db.url"));
System.out.println("db.username = " + reader.getProperty("db.username"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.4 资源管理最佳实践:try-with-resources
JDK 7引入的try-with-resources语句是处理IO资源的最佳实践:
java
// 传统方式(JDK 6及以前)
FileInputStream fis = null;
try {
fis = new FileInputStream("file.txt");
// 读取操作
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// try-with-resources方式(JDK 7+)
try (FileInputStream fis = new FileInputStream("file.txt");
FileOutputStream fos = new FileOutputStream("out.txt")) {
// 读取和写入操作
} catch (IOException e) {
e.printStackTrace();
} // 自动关闭fis和fos,无需finally
优点:
- 代码简洁,避免嵌套的try-catch-finally
- 自动处理关闭顺序(按照资源声明相反的顺序)
- 如果try块和close()都抛出异常,try块的异常会抑制close()的异常
5.5 常见问题与解决方案
问题1:文件被占用(Windows平台)
现象 :在Windows上,如果文件已被其他程序打开,再尝试写入会抛出FileNotFoundException。
解决方案:
- 确保程序逻辑正确释放资源
- 使用
FileChannel的tryLock()尝试获取文件锁 - 考虑使用随机访问文件
RandomAccessFile
问题2:路径不存在
现象 :写入文件时父目录不存在,抛出FileNotFoundException。
解决方案:
java
File file = new File("parent/child/data.txt");
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs(); // 创建所有不存在的父目录
}
try (FileOutputStream fos = new FileOutputStream(file)) {
// 写入数据
}
问题3:编码问题
现象 :使用FileOutputStream写入文本时,出现乱码。
解决方案:
-
明确指定字符编码:
javaString text = "中文内容"; try (FileOutputStream fos = new FileOutputStream("file.txt")) { fos.write(text.getBytes(StandardCharsets.UTF_8)); } -
使用字符流
FileWriter(可指定编码):javatry (FileWriter fw = new FileWriter("file.txt", StandardCharsets.UTF_8)) { fw.write(text); }
问题4:性能瓶颈
现象:读写大文件时速度很慢。
解决方案:
- 使用缓冲流包装:
new BufferedInputStream(new FileInputStream(...)) - 增加缓冲区大小(4KB-64KB)
- 使用NIO的
FileChannel.transferTo()或内存映射文件 - 考虑异步IO(NIO.2)
第六章:总结与展望
6.1 三大核心类对比
| 特性 | File | FileInputStream | FileOutputStream |
|---|---|---|---|
| 主要作用 | 文件和目录路径名操作 | 从文件读取字节数据 | 向文件写入字节数据 |
| 核心功能 | 创建、删除、重命名、查询属性 | 读取字节数组/单个字节 | 写入字节数组/单个字节 |
| 是否处理内容 | 否(只处理元数据) | 是 | 是 |
| 数据流向 | N/A | 文件 → 程序 | 程序 → 文件 |
| 线程安全 | 是(不可变对象) | 否(需要外部同步) | 否(需要外部同步) |
| 异常处理 | 部分方法返回boolean | IOException | IOException |
| JDK版本 | 1.0 | 1.0 | 1.0 |
6.2 从字节流到高级IO的演进
本文深入讲解了File、FileInputStream和FileOutputStream这三个基础的IO类。在实际开发中,我们通常不会直接使用它们,而是基于它们构建更高级的IO处理:
- 缓冲流 :
BufferedInputStream/BufferedOutputStream- 减少系统调用次数 - 数据流 :
DataInputStream/DataOutputStream- 读写Java基本数据类型 - 对象流 :
ObjectInputStream/ObjectOutputStream- 对象序列化 - 字符流 :
FileReader/FileWriter- 专门处理文本文件 - NIO.2 :
Files、Path、FileChannel- 更现代、更强大的文件操作
6.3 未来趋势
随着Java的发展,文件IO也在不断演进:
- JDK 7 NIO.2 :引入了
Path、Files类,提供了更全面的文件操作API - JDK 8 :增强了
Files类的方法,支持Stream API遍历目录 - JDK 11 :
Files.readString()和Files.writeString()简化文本文件读写 - 未来:可能进一步增强异步IO、内存访问等特性
6.4 给开发者的建议
-
掌握基础:深刻理解File、FileInputStream、FileOutputStream,它们是所有Java文件IO的基石
-
使用缓冲:除非处理极小的文件,否则始终用缓冲流包装字节流
-
明确资源管理:始终使用try-with-resources确保资源释放
-
区分字节与字符:处理文本优先考虑字符流或指定编码,处理二进制使用字节流
-
了解NIO:对于高性能要求的场景,学习并应用NIO.2 API
-
阅读源码:通过阅读JDK源码,理解设计模式(装饰器模式)和底层实现
附录:常用代码片段速查
读取文件所有字节
java
byte[] allBytes = Files.readAllBytes(Paths.get("file.dat"));
读取文件所有行
java
List<String> lines = Files.readAllLines(Paths.get("file.txt"), StandardCharsets.UTF_8);
写入字符串到文件
java
Files.write(Paths.get("file.txt"), "Hello".getBytes(StandardCharsets.UTF_8));
遍历目录(Java 8 Stream)
java
Files.list(Paths.get(".")).forEach(System.out::println);
递归遍历目录树
java
Files.walk(Paths.get(".")).filter(Files::isRegularFile).forEach(System.out::println);
临时文件创建
java
Path tempFile = Files.createTempFile("prefix", ".tmp");
文件复制
java
Files.copy(Paths.get("source.txt"), Paths.get("dest.txt"), StandardCopyOption.REPLACE_EXISTING);
获取文件大小
java
long size = Files.size(Paths.get("file.txt"));
检查文件是否存在
java
boolean exists = Files.exists(Paths.get("file.txt"));