跳到主要内容Java IO 详解:File、FileInputStream 与 FileOutputStream | 极客日志Javajava
Java IO 详解:File、FileInputStream 与 FileOutputStream
本文深入讲解 Java IO 体系中的三大核心类:File、FileInputStream 和 FileOutputStream。涵盖流的概念分类、File 类的路径操作与局限性、字节输入输出流的源码剖析及核心方法(read/write/close)、线程安全性分析,以及文件复制、加密等实战示例。重点介绍了 try-with-resources 资源管理最佳实践、缓冲流性能优化及 NIO 演进方向,帮助开发者掌握基础文件 IO 操作技巧。
引言:Java IO 体系与文件操作
在 Java 应用程序开发中,文件输入输出(I/O)是最基础且最常见的操作之一。无论是读取配置文件、处理用户上传的文档、记录日志信息,还是进行数据持久化,都离不开对文件的操作。Java 的 I/O 体系通过"流"(Stream)的抽象,为开发者提供了一套统一而强大的 API 来处理各种设备间的数据传递。
本文将深入剖析 Java 文件 IO 的三大基石:
- File 类:文件和目录路径名的抽象表示,用于文件和目录的创建、删除、查询等操作
- FileInputStream:字节文件输入流,用于从文件中读取原始字节数据
- FileOutputStream:字节文件输出流,用于将原始字节数据写入文件
我们将从源码层面解读其设计原理,探讨核心方法的使用技巧,分析性能优化的关键点,并提供最佳实践指南。
第一章: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 |
- 字节流:处理所有类型的文件(文本、图片、视频、音频等)。因为所有文件在底层都是以字节形式存储的,字节流是通用的。
- 字符流:专门处理文本文件。它内部处理字符编码和解码,方便处理人类可读的文本。
- 节点流:直接连接数据源,是 IO 操作的基础
- 处理流:在节点流之上进行包装,提供缓冲、转换、对象序列化等高级功能(装饰器模式)
1.3 文件 IO 的核心类
- File:代表文件或目录路径,提供文件系统操作(创建、删除、重命名、查询属性等)
- FileInputStream:字节文件输入流,从文件中读取字节数据
- FileOutputStream:字节文件输出流,向文件中写入字节数据
这三者构成了 Java 进行原始文件操作的基础。理解它们的工作原理,是掌握更高级 IO(如缓冲流、对象流、NIO)的前提。
第二章:File 类深度剖析
java.io.File类是 Java IO 体系中唯一代表文件和目录路径名的类,但它不负责文件内容的读写。它更像是一个文件或目录的"名片",记录了路径信息,并提供了对文件元数据(属性)的操作方法。
2.1 类的定义与核心字段
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 类提供了多种构造方法,灵活适应不同的使用场景:
public class FileConstructorDemo {
public static void main(String[] args) {
File file1 = new File("D:\\data\\document.txt");
File file2 = new File("/home/user/document.txt");
String parent = "D:\\data";
String child = "document.txt";
File file3 = new File(parent, child);
File parentDir = new File("D:\\data");
File file4 = new File(parentDir, "document.txt");
}
}
- 创建时机:
new File()只是在内存中创建了一个对象,代表一个路径,并不会在硬盘上实际创建文件或目录。文件是否真正存在,需要通过exists()方法验证。
- 相对路径:相对路径相对于当前工作目录(可通过
System.getProperty("user.dir")获取)。在 IDEA 中,单元测试方法的相对路径相对于当前 module,main 方法的相对路径相对于当前工程。
路径分隔符:Windows 使用反斜杠\,在 Java 字符串中需要转义为\\u005c;Unix/Linux/Mac 使用正斜杠/。更好的做法是使用File.separator常量,它会根据运行平台自动适配:
File file = new File("D:" + File.separator + "data" + File.separator + "document.txt");
2.3 常用 API 详解
File 类的 API 主要分为四大类:获取基本信息、判断功能、列出目录内容、创建删除操作。
2.3.1 获取文件和目录基本信息
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());
System.out.println("路径: " + file.getPath());
System.out.println("绝对路径: " + file.getAbsolutePath());
try {
System.out.println("规范路径: " + file.getCanonicalPath());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("父目录: " + file.getParent());
System.out.println("文件大小: " + file.length() + " 字节");
System.out.println("最后修改时间: " + new Date(file.lastModified()));
}
}
length()返回 0 的情况:文件不存在,或者文件确实为空
lastModified()返回的是从 1970-01-01 UTC 开始的毫秒数
getAbsolutePath()与getCanonicalPath()的区别:绝对路径可能包含.或..,规范路径会解析这些相对引用
2.3.2 判断功能
import java.io.File;
public class FileCheckDemo {
public static void main(String[] args) {
File file = new File("test.txt");
System.out.println("是否存在: " + file.exists());
System.out.println("是否是文件: " + file.isFile());
System.out.println("是否是目录: " + file.isDirectory());
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 列出目录内容
import java.io.File;
public class FileListDemo {
public static void main(String[] args) {
File dir = new File("D:\\data");
if (dir.exists() && dir.isDirectory()) {
String[] fileNames = dir.list();
System.out.println("目录中的文件和子目录:");
for (String name : fileNames) {
System.out.println(" " + name);
}
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 创建与删除
import java.io.File;
import java.io.IOException;
public class FileCreateDeleteDemo {
public static void main(String[] args) {
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();
}
File singleDir = new File("mydir");
if (singleDir.mkdir()) {
System.out.println("目录创建成功:" + singleDir.getName());
} else {
System.out.println("目录创建失败(可能已存在或父目录不存在)");
}
File multiDir = new File("parent/child/grandchild");
if (multiDir.mkdirs()) {
System.out.println("多级目录创建成功:" + multiDir.getPath());
} else {
System.out.println("多级目录创建失败");
}
if (file.delete()) {
System.out.println("文件删除成功");
} else {
System.out.println("文件删除失败(可能不存在或无权限)");
}
File tempFile = new File("temp.txt");
try {
if (tempFile.createNewFile()) {
tempFile.deleteOnExit();
System.out.println("临时文件将在程序退出时删除");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
createNewFile():原子操作,检查文件是否存在并创建,返回 boolean 表示是否成功创建
mkdir() vs mkdirs():前者要求父目录必须存在,后者会创建所有不存在的父目录
delete():直接删除,不走回收站,需谨慎操作
deleteOnExit():注册一个钩子,在 JVM 正常退出时删除文件,适用于临时文件清理
2.3.5 重命名与移动
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:递归遍历目录树
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:文件过滤器实现
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");
String[] images = dir.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".jpg") || name.endsWith(".png");
}
});
File[] largeFiles = dir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isFile() && pathname.length() > 1024 * 1024;
}
});
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 类定义与继承体系
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 核心字段
public class FileInputStream extends InputStream {
private final FileDescriptor fd;
private final String path;
private FileChannel channel = null;
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 提供了三种重载的构造方法:
public FileInputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null);
}
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);
}
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
private native void open(String name) throws FileNotFoundException;
这个 native 方法会调用操作系统的 API(如 Windows 的CreateFile,Linux 的open)来打开文件,获取文件句柄存储在fd中。
3.3 核心方法详解
3.3.1 read():读取单个字节
public int read() throws IOException {
return read0();
}
private native int read0() throws IOException;
- 每次调用读取一个字节(8 位)
- 返回值范围:0 到 255(无符号字节)
- 如果到达文件末尾,返回 -1
- 该方法会阻塞,直到有数据可读、到达文件末尾或发生异常
- 底层通过 native 方法直接调用操作系统读文件的系统调用
性能考量:每次读取一个字节意味着每个字节都要进行一次系统调用,对于大文件来说效率极低。实际开发中几乎不使用此方法读取大量数据。
3.3.2 read(byte[] b):读取到字节数组
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,将数据存入数组的指定位置
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) {
processData(buffer, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void processData(byte[] data, int length) {
System.out.println("读取到 " + length + " 字节");
}
}
重要提示:fis.read(buffer)返回的是实际读取的字节数,这个数值可能小于 buffer 的长度(特别是在接近文件末尾时)。处理数据时必须使用返回的长度,而不是buffer.length。
3.3.3 skip(long n):跳过字节
public native long skip(long n) throws IOException;
- 跳过并丢弃输入流中的 n 个字节
- 返回实际跳过的字节数(可能小于 n)
- 如果 n 为负数,某些平台支持回退(如例子中
skip(-1))
- 跳过文件末尾不会抛出异常,但后续 read 会返回 -1
FileInputStream fis = new FileInputStream("data.bin");
fis.skip(10);
int b = fis.read();
3.3.4 available():可用字节数估计
public native int available() throws IOException;
- 返回估计的剩余可读取字节数(不受阻塞)
- 这是一个估计值,不保证精确
- 通常用于判断是否需要创建缓冲区,但不能依赖它作为文件总长度的准确值
- 文件超过 EOF 时返回 0
FileInputStream fis = new FileInputStream("file.txt");
byte[] data = new byte[fis.available()];
fis.read(data);
正确用法:仅用于非阻塞场景的提示,不能替代循环读取。
3.3.5 close():关闭流
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():获取文件通道
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),每个线程读取时开始的位置是不可预知的。
public class FileThreadTest implements Runnable {
private int type;
private int gap;
private FileInputStream in;
@Override
public void run() {
byte[] body = new byte[gap];
if (this.type == 0) {
} else {
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)
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("input.dat")) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
System.out.println("读取了 " + bytesRead + " 字节");
}
} catch (IOException e) {
System.err.println("文件读取错误:" + e.getMessage());
}
}
}
示例 2:复制文件(结合 FileOutputStream)
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];
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:读取部分数据(指定偏移量)
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];
byte[] body = new byte[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 性能优化建议
- 合理设置缓冲区大小:通常 4KB-64KB 之间,具体取决于应用场景
- 避免在循环中使用单字节读取:
while ((b = fis.read()) != -1)是性能杀手
- 考虑使用 NIO:对于大文件或需要高吞吐量的场景,
FileChannel和内存映射文件性能更好
使用缓冲流:FileInputStream每次读取都会触发系统调用。包装为BufferedInputStream可大幅减少系统调用次数:
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("large.dat"))) {
}
第四章:FileOutputStream 源码剖析与使用
java.io.FileOutputStream是字节文件输出流,用于将原始字节数据写入文件。它是OutputStream抽象类的直接子类,与FileInputStream对应。
4.1 类定义与继承体系
public class FileOutputStream extends OutputStream
java.lang.Object └── java.io.OutputStream └── java.io.FileOutputStream
4.2 核心字段与构造方法
4.2.1 核心字段
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;
}
- boolean append:标记是否为追加模式。
true表示写入的数据追加到文件末尾,false表示覆盖文件开头。
4.2.2 构造方法
FileOutputStream 提供了 5 个重载的构造方法:
public FileOutputStream(String name) throws FileNotFoundException {
this(name != null ? new File(name) : null, false);
}
public FileOutputStream(String name, boolean append) throws FileNotFoundException {
this(name != null ? new File(name) : null, append);
}
public FileOutputStream(File file) throws FileNotFoundException {
this(file, false);
}
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);
}
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);
}
private native void open(String name, boolean append) throws FileNotFoundException;
append = false(默认):文件指针定位到文件开头。如果文件已存在,原有内容会被新写入的内容覆盖
append = true:文件指针定位到文件末尾,新写入的内容追加到原内容之后
4.3 核心方法详解
4.3.1 write(int b):写入单个字节
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):写入字节数组
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():关闭流
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()
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:基本文件写入
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());
System.out.println("数据写入成功");
} catch (IOException e) {
System.err.println("写入失败:" + e.getMessage());
}
}
}
示例 2:追加模式 vs 覆盖模式
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();
}
}
}
示例 3:写入二进制数据
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 value = 12345678;
fos.write((value >>> 24) & 0xFF);
fos.write((value >>> 16) & 0xFF);
fos.write((value >>> 8) & 0xFF);
fos.write(value & 0xFF);
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.5 注意事项与常见陷阱
- 自动创建文件:如果输出文件不存在,
FileOutputStream会自动创建它(前提是父目录存在)
- 目录 vs 文件:如果指定的路径是一个已存在的目录,会抛出
FileNotFoundException
- 权限问题:如果没有写入权限,也会抛出
FileNotFoundException
- 覆盖 vs 追加:默认为覆盖模式,需要追加时务必使用带
boolean append参数的构造方法
- 数据持久性:
write()方法返回时,数据不一定已经持久化到磁盘,可能还在操作系统缓存中。需要确保数据真正写入可调用getFD().sync()
- 多线程写入:与
FileInputStream类似,多线程共享同一个FileOutputStream会导致数据交错,需要外部同步或使用每个线程独立的流
第五章:综合实战与最佳实践
5.1 文件复制工具完整实现
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 {
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 (FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(destFile)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
fos.flush();
}
}
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();
}
}
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);
}
}
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");
start = System.currentTimeMillis();
copyByChannel(src, dest + ".channel");
end = System.currentTimeMillis();
System.out.println("NIO 通道耗时:" + (end - start) + "ms");
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 文件加密/解密示例(异或算法)
import java.io.*;
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 配置文件读取示例
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);
}
}
public void saveConfig(String configFile) throws IOException {
try (FileOutputStream fos = new FileOutputStream(configFile)) {
props.store(fos, "Configuration File");
}
}
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 资源的最佳实践:
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 (FileInputStream fis = new FileInputStream("file.txt");
FileOutputStream fos = new FileOutputStream("out.txt")) {
} catch (IOException e) {
e.printStackTrace();
}
- 代码简洁,避免嵌套的 try-catch-finally
- 自动处理关闭顺序(按照资源声明相反的顺序)
- 如果 try 块和 close() 都抛出异常,try 块的异常会抑制 close() 的异常
5.5 常见问题与解决方案
问题 1:文件被占用(Windows 平台)
现象:在 Windows 上,如果文件已被其他程序打开,再尝试写入会抛出FileNotFoundException。
- 确保程序逻辑正确释放资源
- 使用
FileChannel的tryLock()尝试获取文件锁
- 考虑使用随机访问文件
RandomAccessFile
问题 2:路径不存在
现象:写入文件时父目录不存在,抛出FileNotFoundException。
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写入文本时,出现乱码。
try (FileWriter fw = new FileWriter("file.txt", StandardCharsets.UTF_8)) {
fw.write(text);
}
String text = "中文内容";
try (FileOutputStream fos = new FileOutputStream("file.txt")) {
fos.write(text.getBytes(StandardCharsets.UTF_8));
}
问题 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 源码,理解设计模式(装饰器模式)和底层实现
附录:常用代码片段速查
读取文件所有字节
byte[] allBytes = Files.readAllBytes(Paths.get("file.dat"));
读取文件所有行
List<String> lines = Files.readAllLines(Paths.get("file.txt"), StandardCharsets.UTF_8);
写入字符串到文件
Files.write(Paths.get("file.txt"), "Hello".getBytes(StandardCharsets.UTF_8));
遍历目录(Java 8 Stream)
Files.list(Paths.get(".")).forEach(System.out::println);
递归遍历目录树
Files.walk(Paths.get(".")).filter(Files::isRegularFile).forEach(System.out::println);
临时文件创建
Path tempFile = Files.createTempFile("prefix", ".tmp");
文件复制
Files.copy(Paths.get("source.txt"), Paths.get("dest.txt"), StandardCopyOption.REPLACE_EXISTING);
获取文件大小
long size = Files.size(Paths.get("file.txt"));
检查文件是否存在
boolean exists = Files.exists(Paths.get("file.txt"));
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online