Commons IO 与 IO 综合应用 | JavaSE
Commons IO 与 IO 综合应用
一、学习目标
完成本章后,你应该能够:
- 能够解释 Apache Commons IO 是什么,以及它与 JDK 原生 IO/NIO 的关系。
- 能够在 Maven 项目中引入 Commons IO。
- 能够使用
FileUtils完成文件复制、目录复制、目录删除等常见操作。 - 能够使用
FileUtils按指定字符集读取和写入文本。 - 能够判断什么时候适合直接使用 Commons IO,什么时候应该使用 JDK 原生 IO 流。
- 能够从需求出发正确选择字节流、字符流、缓冲流、转换流、打印流和数据流。
- 能够使用
try-with-resources管理 IO 资源。 - 能够综合 File、递归、字符集、集合、Stream 与 IO 完成实际文件处理任务。
- 能够建立完整的 Java File 与 IO 知识体系。
二、核心知识
2.1 为什么还需要 Commons IO
到现在为止,我们已经可以自己完成文件复制。
例如:
try (
BufferedInputStream input =
new BufferedInputStream(
new FileInputStream("source.jpg")
);
BufferedOutputStream output =
new BufferedOutputStream(
new FileOutputStream("target.jpg")
)
) {
byte[] buffer = new byte[8192];
int len;
while ((len = input.read(buffer)) != -1) {
output.write(buffer, 0, len);
}
}
代码没有问题。
但如果实际开发中经常需要:
复制文件
复制整个目录
删除整个目录
读取整个文本文件
写入整个文本文件
获取文件大小
遍历文件
每一次都重新编写:
创建流
↓
循环读取
↓
循环写入
↓
处理目录
↓
递归
↓
资源释放
↓
异常处理
会出现大量重复代码。
于是就产生一个非常自然的工程思想:
把大量重复、通用而且已经成熟的 IO 操作封装成工具 API。
Apache Commons IO 就是用于解决这类问题的成熟 Java IO 工具库。
2.2 什么是 Apache Commons IO
Apache Commons IO 是 Apache 软件基金会旗下的一个 Java IO 工具库。
它并不是:
JDK 自带类库
而属于:
第三方开源库
它在 Java 原生:
java.io
java.nio
java.nio.file
能力之上,封装了大量常用操作。
可以粗略理解:
JDK IO / NIO
↓
提供基础能力
Commons IO
↓
封装大量常用操作
↓
减少模板代码
例如原来复制文件可能需要十几行:
InputStream
+
OutputStream
+
byte[]
+
while
+
try-with-resources
使用 Commons IO:
FileUtils.copyFile(source, target);
即可表达整个业务意图。
这就是工具库的价值:
不是提供计算机本来不能做的事情,而是把重复的底层操作封装起来,提高开发效率和代码可读性。
2.3 Commons IO 不等于 Java IO
这一点必须分清。
Java 原生 IO:
InputStream
OutputStream
Reader
Writer
File
Files
Path
属于:
JDK
Commons IO:
FileUtils
IOUtils
FilenameUtils
FileSystemUtils
...
来自:
Apache Commons IO
因此:
import org.apache.commons.io.FileUtils;
必须先在项目中加入 Commons IO 依赖。
否则 IDE 会出现类似:
Cannot resolve symbol 'FileUtils'
2.4 Commons IO 最重要的 FileUtils
当前阶段最需要掌握的是:
org.apache.commons.io.FileUtils
它是:
面向文件与目录操作的工具类。
原教学体系中重点要求掌握:
| 方法 | 作用 |
| ------------------------ | ------------------ |
| copyFile(...) | 复制文件 |
| copyDirectory(...) | 复制目录 |
| deleteDirectory(...) | 删除目录 |
| readFileToString(...) | 将文件读取成字符串 |
| writeStringToFile(...) | 将字符串写入文件 |
不要试图背完整个 FileUtils。
学习工具类真正应该掌握的是:
知道它解决什么问题
+
知道典型 API
+
需要时能够查 API 文档
而不是:
把一个工具类的一百多个方法全部背下来。
三、使用方法
3.1 Maven 引入 Commons IO
如果项目使用 Maven,可以在:
pom.xml
中加入依赖:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.22.0</version>
</dependency>
然后:
import org.apache.commons.io.FileUtils;
即可使用。
完整关系:
pom.xml
↓
声明 commons-io
↓
Maven 下载 jar
↓
加入项目 classpath
↓
import FileUtils
↓
调用 API
3.2 使用 FileUtils.copyFile() 复制文件
原始 IO:
try (
InputStream input =
new BufferedInputStream(
new FileInputStream("source.jpg")
);
OutputStream output =
new BufferedOutputStream(
new FileOutputStream("target.jpg")
)
) {
byte[] buffer = new byte[8192];
int len;
while ((len = input.read(buffer)) != -1) {
output.write(buffer, 0, len);
}
}
Commons IO:
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
public class CopyFileDemo {
public static void main(String[] args) throws IOException {
File source =
new File("data/source.jpg");
File target =
new File("backup/source.jpg");
FileUtils.copyFile(source, target);
}
}
核心代码只有:
FileUtils.copyFile(source, target);
语义非常明确:
复制 source
↓
到 target
3.3 copyFile() 仍然是字节复制
虽然我们没有手写:
InputStream
OutputStream
byte[]
但这并不意味着:
文件复制已经不需要字节了。
磁盘中的文件最终依然是:
byte
Commons IO 只是替我们:
封装了读取
+
封装了写入
+
封装了资源管理
+
封装了大量边界处理
所以学习 Commons IO 之前必须先学原生 IO。
否则只会:
FileUtils.copyFile(...)
却不知道:
它解决什么问题
为什么图片不能用字符流复制
为什么 IO 会有异常
为什么需要关闭资源
这就是“会调用 API”和“理解技术”的区别。
3.4 使用 copyDirectory() 复制目录
以前复制目录需要:
遍历目录
↓
判断文件/文件夹
↓
创建目标目录
↓
递归
↓
复制文件
Commons IO:
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
public class CopyDirectoryDemo {
public static void main(String[] args) throws IOException {
File sourceDir =
new File("data");
File targetDir =
new File("backup");
FileUtils.copyDirectory(
sourceDir,
targetDir
);
}
}
它会递归处理源目录中的内容。
可以抽象成:
data
├── article.txt
├── image.jpg
└── docs
├── java.md
└── mysql.md
经过复制:
backup
├── article.txt
├── image.jpg
└── docs
├── java.md
└── mysql.md
这正是我们之前:
File
+
listFiles()
+
递归
+
字节流
手写实现过的能力。
3.5 copyDirectory() 和 copyDirectoryToDirectory()
这两个方法容易混淆。
假设:
source
├── a.txt
└── b.txt
目标:
backup
调用:
FileUtils.copyDirectory(
new File("source"),
new File("backup")
);
核心语义是:
将
source中的内容复制到backup中。
结果类似:
backup
├── a.txt
└── b.txt
而:
FileUtils.copyDirectoryToDirectory(
new File("source"),
new File("backup")
);
表示:
把
source这个目录本身复制到backup目录下面。
结果:
backup
└── source
├── a.txt
└── b.txt
因此:
copyDirectory
=
复制目录内容到目标目录
copyDirectoryToDirectory
=
把整个源目录放到目标目录中
实际使用时必须先理解目标目录结构。
3.6 删除整个目录
原生 Java 删除目录时:
file.delete();
如果目录中还有内容:
通常不能直接删除
需要:
递归删除所有子文件
↓
递归删除所有子目录
↓
最后删除当前目录
Commons IO:
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
public class DeleteDirectoryDemo {
public static void main(String[] args) throws IOException {
File directory =
new File("temp");
FileUtils.deleteDirectory(directory);
}
}
一句:
FileUtils.deleteDirectory(directory);
即可递归删除整个目录。
3.7 删除目录是高风险操作
假设:
FileUtils.deleteDirectory(
new File("D:/")
);
那么操作目标就完全不同了。
因此真实项目中进行删除操作之前至少应该确认:
路径是否正确
↓
路径是否存在
↓
是否真的是目标目录
↓
是否允许删除
↓
是否需要二次确认
例如:
File directory =
new File("data/temp");
if (directory.exists()
&& directory.isDirectory()) {
FileUtils.deleteDirectory(directory);
}
但即使这样,也不能代替:
正确设计删除边界。
文件系统操作通常是:
不可逆或高成本操作
所以一定要谨慎。
3.8 将整个文本文件读取成 String
原生写法可能是:
StringBuilder builder = new StringBuilder();
try (
BufferedReader reader =
new BufferedReader(
new FileReader(
"article.txt",
StandardCharsets.UTF_8
)
)
) {
String line;
while ((line = reader.readLine()) != null) {
builder.append(line);
builder.append(System.lineSeparator());
}
}
Commons IO:
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class ReadTextDemo {
public static void main(String[] args) throws IOException {
File file =
new File("article.txt");
String content =
FileUtils.readFileToString(
file,
StandardCharsets.UTF_8
);
System.out.println(content);
}
}
核心:
String content =
FileUtils.readFileToString(
file,
StandardCharsets.UTF_8
);
3.9 为什么读取文本必须考虑 Charset
假设:
article.txt
真实字符集是:
UTF-8
那么应该:
FileUtils.readFileToString(
file,
StandardCharsets.UTF_8
);
过程仍然是:
文件 byte
↓
UTF-8 解码
↓
Java String
Commons IO 并没有消灭:
编码 / 解码
它只是封装了操作过程。
所以前面的:
字符集
编码
解码
乱码
转换流
知识依然有效。
3.10 String 写入文件
例如:
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class WriteTextDemo {
public static void main(String[] args) throws IOException {
File file =
new File("result.txt");
String content =
"Java\nMySQL\nSpring Boot";
FileUtils.writeStringToFile(
file,
content,
StandardCharsets.UTF_8
);
}
}
数据过程:
Java String
↓
UTF-8 编码
↓
byte
↓
result.txt
3.11 追加写入
如果希望追加:
FileUtils.writeStringToFile(
file,
content,
StandardCharsets.UTF_8,
true
);
最后一个:
true
表示:
append
=
追加
如果:
false
则不是追加模式。
3.12 读取多行数据
除了:
readFileToString()
Commons IO 还可以按行读取:
List<String> lines =
FileUtils.readLines(
file,
StandardCharsets.UTF_8
);
例如:
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class ReadLinesDemo {
public static void main(String[] args) throws Exception {
List<String> lines =
FileUtils.readLines(
new File("students.txt"),
StandardCharsets.UTF_8
);
for (String line : lines) {
System.out.println(line);
}
}
}
现在可以直接得到:
文件
↓
List<String>
然后继续:
集合
+
Lambda
+
Stream
进行数据处理。
四、原理与进阶
4.1 Commons IO 是封装,不是魔法
下面代码:
FileUtils.copyFile(source, target);
看起来像:
一句代码复制文件
但底层仍然必须完成:
打开源文件
↓
读取字节
↓
创建目标文件
↓
写入字节
↓
处理异常
↓
关闭资源
所以应该建立:
高层 API
↓
封装底层通用过程
而不是:
高层 API
↓
绕过 IO 原理
4.2 为什么先学 IO,再学 Commons IO
如果一开始只学习:
FileUtils.copyFile(...)
你可能能复制文件。
但面对:
为什么中文乱码?
为什么图片不能用 Reader?
为什么网络数据也是 InputStream?
为什么需要 try-with-resources?
为什么读取大文件不能直接 readFileToString?
为什么目录复制涉及递归?
就很难解释。
所以合理学习顺序是:
File
↓
递归
↓
字符集
↓
IO 体系
↓
字节流
↓
字符流
↓
缓冲流
↓
转换流
↓
打印流 / 数据流
↓
资源释放
↓
Commons IO
最后才真正获得:
既理解底层,也能够高效开发。
4.3 Commons IO 与 JDK Files
Commons IO 并不是 Java 唯一的便捷文件操作方案。
JDK 自身还有:
java.nio.file.Files
例如:
Files.copy(sourcePath, targetPath);
可以复制文件。
读取字符串:
String content =
Files.readString(
path,
StandardCharsets.UTF_8
);
写字符串:
Files.writeString(
path,
content,
StandardCharsets.UTF_8
);
所以实际开发中可能同时看到:
java.io
java.nio.file
Apache Commons IO
三种体系。
可以粗略理解:
java.io
→ 经典 IO 流体系
→ 理解 IO 基础的核心
java.nio.file
→ JDK 原生现代文件系统 API
Commons IO
→ 第三方工具库
→ 对大量常见 IO 场景进一步封装
不是:
学了 Commons IO
→ java.io 全部废弃
也不是:
有 Files
→ Commons IO 完全没有价值
正确思想是:
根据实际问题选择抽象层次最合适的 API。
4.4 工具类最重要的是降低重复复杂度
假设业务要求:
复制整个用户上传目录。
如果自己实现,需要考虑:
目录是否存在
目录创建
文件与目录判断
递归
文件复制
异常
资源释放
如果这些全部属于:
通用基础能力
那么在业务项目里反复手写:
没有太大价值
使用成熟工具:
FileUtils.copyDirectory(...)
能够让业务代码更关注:
为什么复制
复制谁
复制到哪里
失败以后怎么办
这体现的其实是一种非常重要的软件工程思想:
复用可靠的基础设施,让代码聚焦业务。
4.5 readFileToString() 为什么不适合任意大文件
代码:
String content =
FileUtils.readFileToString(
file,
StandardCharsets.UTF_8
);
意味着:
整个文件
↓
一次形成一个 String
↓
进入 JVM 内存
如果文件只有:
5 KB
100 KB
1 MB
通常非常方便。
但是如果文件:
5 GB
20 GB
100 GB
显然不能简单认为:
一把读进 String
是合理方案。
这时应该考虑:
流式读取
例如:
BufferedReader
逐行处理:
读一行
↓
处理一行
↓
丢弃不需要的数据
↓
继续下一行
因此:
API 越方便,不代表任何数据规模下都应该使用。
4.6 工具类无法替你决定业务语义
例如:
FileUtils.copyFile(source, target);
Commons IO 可以解决:
怎么复制
但它不能替你决定:
目标文件已存在怎么办?
应该覆盖吗?
要不要生成新名称?
是否允许复制?
用户有没有权限?
失败是否回滚?
是否记录日志?
这些仍然属于:
业务设计
所以软件开发永远不是:
找到一个 API,问题就全部结束。
五、IO 综合应用
5.1 Java IO 到底应该怎么选
现在已经学习了大量 IO 类。
如果只背类名,很容易混乱。
真正应该从:
我要处理什么数据?
开始。
5.2 第一步:文件还是目录
如果只是表示:
文件路径
目录路径
首先想到:
File
或者现代 JDK:
Path
例如:
判断是否存在
判断文件还是目录
获取文件大小
列出目录内容
这是:
文件系统对象操作
还没有进入数据读写。
5.3 第二步:处理二进制还是文本
如果是:
图片
视频
PDF
压缩包
音频
.class
.exe
任意未知类型文件
优先:
InputStream
OutputStream
也就是:
字节流
典型:
BufferedInputStream
BufferedOutputStream
如果是:
TXT
CSV
Java 源码
Markdown
JSON 文本
XML 文本
日志
并且真正需要:
读取字符内容
优先考虑:
Reader
Writer
也就是:
字符流
典型:
BufferedReader
BufferedWriter
5.4 第三步:是否需要指定字符集
如果:
外部文本编码明确
例如:
UTF-8
GBK
必须关注:
编码 / 解码
可以使用:
InputStreamReader
OutputStreamWriter
完成:
byte ↔ char
或者现代 JDK 的:
FileReader(..., Charset)
FileWriter(..., Charset)
5.5 第四步:是否需要方便打印
如果需求是:
日志
报表
格式化输出
文本结果
可以考虑:
PrintStream
PrintWriter
例如:
writer.printf(
"%s,%d,%.2f%n",
name,
age,
score
);
5.6 第五步:是否需要保存 Java 基本类型
如果需求是:
int
double
boolean
String
按照确定协议:
以二进制形式保存和恢复
可以:
DataOutputStream
DataInputStream
但是一定遵守:
写入顺序
=
读取顺序
5.7 第六步:是否只是常见文件操作
如果需求只是:
复制文件
复制目录
删除目录
整个文本读取
整个文本写入
可以考虑:
Files
或者:
Commons IO FileUtils
没有必要每一次都重新手写底层循环。
5.8 第七步:资源怎么管理
只要使用:
InputStream
OutputStream
Reader
Writer
等需要关闭的资源,优先:
try-with-resources
例如:
try (
BufferedReader reader = ...
) {
}
而不是:
创建资源
↓
使用
↓
祈祷中间不出异常
↓
最后手动 close
5.9 Java IO 选型总图
可以把整个 File 与 IO 阶段压缩成下面这张图:
我要处理文件
│
▼
路径/目录操作?
│ │
是 否
│ │
File / Path │
▼
要读写数据吗?
│
▼
┌──────────┴──────────┐
│ │
二进制 文本
│ │
▼ ▼
InputStream Reader
OutputStream Writer
│ │
▼ ▼
BufferedInputStream BufferedReader
BufferedOutputStream BufferedWriter
│
▼
字符集不一致?
│ │
是 否
│
▼
InputStreamReader
OutputStreamWriter
其他特殊需求:
格式化文本输出
→ PrintStream / PrintWriter
保存基本数据类型
→ DataOutputStream / DataInputStream
常用文件/目录工具操作
→ Files / Commons IO FileUtils
资源管理
→ try-with-resources
真正掌握这张图,比单纯背二十个 IO 类重要得多。
六、综合案例:文件备份与统计工具
现在将整个 File 与 IO 阶段真正串起来。
6.1 需求分析
假设我们有:
workspace
├── README.md
├── notes.txt
├── image
│ ├── java.png
│ └── mysql.png
└── code
├── Main.java
└── Student.java
开发一个:
文件备份与统计工具
要求:
-
判断源目录是否存在。
-
将整个目录备份到
backup。 -
递归统计:
- 文件总数;
- 目录总数;
- 总字节数。
-
找出所有:
.java.md.txt
文本文件。
-
统计这些文本文件总行数。
-
将统计结果写入:
backup-report.txt
- 所有文本统一按照:
UTF-8
处理。
这个案例可以把前面的大部分知识串起来。
6.2 技术选型
目录表示:
File
目录备份:
FileUtils.copyDirectory()
递归遍历:
File.listFiles()
+
递归
文本行读取:
BufferedReader
字符集:
StandardCharsets.UTF_8
报告输出:
PrintWriter
资源释放:
try-with-resources
集合处理:
List<File>
这就是:
File
+
递归
+
IO
+
字符集
+
集合
+
Commons IO
的综合应用。
6.3 数据模型
定义统计结果:
public class FileStatistics {
private int fileCount;
private int directoryCount;
private long totalBytes;
private long textLineCount;
public int getFileCount() {
return fileCount;
}
public void increaseFileCount() {
fileCount++;
}
public int getDirectoryCount() {
return directoryCount;
}
public void increaseDirectoryCount() {
directoryCount++;
}
public long getTotalBytes() {
return totalBytes;
}
public void addBytes(long bytes) {
totalBytes += bytes;
}
public long getTextLineCount() {
return textLineCount;
}
public void addTextLines(long lines) {
textLineCount += lines;
}
}
这里体现:
让对象保存统计状态,而不是把所有变量塞进
main()。
6.4 主流程设计
程序流程:
启动程序
↓
检查 source
↓
不存在?
┌─┴─┐
是 否
│ │
结束 ↓
备份目录
↓
递归统计
↓
文本行统计
↓
生成报告
↓
结束
6.5 判断文本文件
public static boolean isTextFile(File file) {
String name =
file.getName().toLowerCase();
return name.endsWith(".txt")
|| name.endsWith(".md")
|| name.endsWith(".java");
}
6.6 递归统计目录
public static void scan(
File file,
FileStatistics statistics
) throws IOException {
if (file.isFile()) {
statistics.increaseFileCount();
statistics.addBytes(
file.length()
);
if (isTextFile(file)) {
statistics.addTextLines(
countLines(file)
);
}
return;
}
if (file.isDirectory()) {
statistics.increaseDirectoryCount();
File[] children =
file.listFiles();
if (children == null) {
return;
}
for (File child : children) {
scan(
child,
statistics
);
}
}
}
这里重新应用:
递归出口
+
目录遍历
+
File
6.7 统计文本行数
public static long countLines(
File file
) throws IOException {
long count = 0;
try (
BufferedReader reader =
new BufferedReader(
new FileReader(
file,
StandardCharsets.UTF_8
)
)
) {
while (reader.readLine() != null) {
count++;
}
}
return count;
}
这里应用:
字符流
+
缓冲流
+
UTF-8
+
try-with-resources
6.8 生成统计报告
public static void writeReport(
FileStatistics statistics,
File report
) throws IOException {
try (
PrintWriter writer =
new PrintWriter(
report,
StandardCharsets.UTF_8
)
) {
writer.println("文件备份统计报告");
writer.println("====================");
writer.println(
"文件数量:" +
statistics.getFileCount()
);
writer.println(
"目录数量:" +
statistics.getDirectoryCount()
);
writer.println(
"总字节数:" +
statistics.getTotalBytes()
);
writer.println(
"文本总行数:" +
statistics.getTextLineCount()
);
}
}
这里应用:
PrintWriter
+
Charset
+
try-with-resources
6.9 完整主程序
import org.apache.commons.io.FileUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
public class IoComprehensiveDemo {
public static void main(String[] args) {
File source =
new File("workspace");
File backup =
new File("backup");
File report =
new File("backup-report.txt");
if (!source.exists()
|| !source.isDirectory()) {
System.out.println(
"源目录不存在或不是目录"
);
return;
}
try {
FileUtils.copyDirectory(
source,
backup
);
FileStatistics statistics =
new FileStatistics();
scan(
source,
statistics
);
writeReport(
statistics,
report
);
System.out.println(
"备份与统计完成"
);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void scan(
File file,
FileStatistics statistics
) throws IOException {
if (file.isFile()) {
statistics.increaseFileCount();
statistics.addBytes(
file.length()
);
if (isTextFile(file)) {
statistics.addTextLines(
countLines(file)
);
}
return;
}
if (file.isDirectory()) {
statistics.increaseDirectoryCount();
File[] children =
file.listFiles();
if (children == null) {
return;
}
for (File child : children) {
scan(
child,
statistics
);
}
}
}
public static boolean isTextFile(
File file
) {
String name =
file.getName()
.toLowerCase();
return name.endsWith(".txt")
|| name.endsWith(".md")
|| name.endsWith(".java");
}
public static long countLines(
File file
) throws IOException {
long count = 0;
try (
BufferedReader reader =
new BufferedReader(
new FileReader(
file,
StandardCharsets.UTF_8
)
)
) {
while (
reader.readLine() != null
) {
count++;
}
}
return count;
}
public static void writeReport(
FileStatistics statistics,
File report
) throws IOException {
try (
PrintWriter writer =
new PrintWriter(
report,
StandardCharsets.UTF_8
)
) {
writer.println(
"文件备份统计报告"
);
writer.println(
"===================="
);
writer.println(
"文件数量:" +
statistics.getFileCount()
);
writer.println(
"目录数量:" +
statistics.getDirectoryCount()
);
writer.println(
"总字节数:" +
statistics.getTotalBytes()
);
writer.println(
"文本总行数:" +
statistics.getTextLineCount()
);
}
}
}
统计类:
public class FileStatistics {
private int fileCount;
private int directoryCount;
private long totalBytes;
private long textLineCount;
public int getFileCount() {
return fileCount;
}
public void increaseFileCount() {
fileCount++;
}
public int getDirectoryCount() {
return directoryCount;
}
public void increaseDirectoryCount() {
directoryCount++;
}
public long getTotalBytes() {
return totalBytes;
}
public void addBytes(long bytes) {
totalBytes += bytes;
}
public long getTextLineCount() {
return textLineCount;
}
public void addTextLines(long lines) {
textLineCount += lines;
}
}
这个案例最重要的并不是代码本身。
而是你能够看出:
目录备份
→ Commons IO
目录扫描
→ File + 递归
文本判断
→ String API
文本读取
→ BufferedReader
字符集
→ UTF-8
资源管理
→ try-with-resources
状态存储
→ Java 对象
报告输出
→ PrintWriter
这才叫:
综合应用。
六、常见问题
6.1 Commons IO 是 JDK 自带的吗?
不是。
它是:
Apache 第三方开源库
所以必须:
引入依赖
以后才能:
import org.apache.commons.io.FileUtils;
6.2 有 Commons IO 后还需要学习原生 IO 吗?
需要。
而且非常重要。
Commons IO:
解决开发效率
原生 IO:
解决原理理解
+
底层能力
+
复杂场景
+
API 适配
例如网络编程中:
socket.getInputStream()
返回的仍然是:
InputStream
你必须理解:
字节流
缓冲流
转换流
字符集
才能正确处理。
6.3 FileUtils.copyFile() 能复制图片吗?
可以。
它本质完成:
字节复制
因此:
图片
视频
PDF
压缩包
文本
等普通文件都可以进行复制。
复制操作并不需要把文件内容解释成字符。
6.4 readFileToString() 可以读取图片吗?
从 API 参数角度它接收 File。
但从业务语义上:
不应该把图片当文本读取。
因为:
readFileToString
意味着:
文件 byte
↓
字符集解码
↓
String
图片本身不是文本数据。
应该使用:
字节处理
而不是:
字符解码
6.5 为什么推荐显式指定 Charset?
不要简单写成:
依赖默认字符集
更清晰的方式:
StandardCharsets.UTF_8
因为这样代码明确表达:
这个文件的编码协议就是 UTF-8
能够显著减少环境差异与乱码问题。
6.6 FileUtils 和 Files 应该选谁?
没有绝对答案。
例如简单复制一个文件:
Files.copy(...)
已经非常方便,就没有必要为了这一行代码强制增加第三方依赖。
但是如果项目本身已经使用 Commons IO,并且需要:
目录递归复制
递归删除
丰富的文件过滤
流工具
文件比较
大量文件工具操作
Commons IO 会非常方便。
原则:
优先选择能够清晰解决问题、依赖合理、团队能够维护的方案。
6.7 Commons IO 会自动处理所有异常吗?
不会。
例如:
FileUtils.copyFile(...)
仍然可能因为:
源文件不存在
权限不足
目标不可写
磁盘异常
路径非法
产生异常。
因此:
FileUtils.copyFile(...)
不是:
永远成功
它只是:
把大量 IO 实现细节封装了。
异常处理仍然是调用者的重要职责。
6.8 为什么工具库版本也要管理?
因为 Commons IO 属于:
外部依赖
随着版本变化可能出现:
新增 API
废弃 API
行为改进
漏洞修复
兼容性变化
所以真实项目应该:
使用 Maven / Gradle 管理依赖
+
明确版本
+
升级时查看官方 Release Notes
而不是随便从网上下载:
一个不知道版本的 jar
丢到项目里。
七、练习与验收
7.1 知识问答
- [ ] Apache Commons IO 是什么?
- [ ] Commons IO 是 JDK 自带的吗?
- [ ] 为什么已经有 Java IO 还需要 Commons IO?
- [ ]
FileUtils的主要作用是什么? - [ ] 如何使用
copyFile()复制文件? - [ ] 如何使用
copyDirectory()复制整个目录? - [ ]
copyDirectory()与copyDirectoryToDirectory()有什么区别? - [ ] 如何递归删除整个目录?
- [ ]
readFileToString()适合处理什么数据? - [ ] 为什么读取文本仍然必须考虑字符集?
- [ ] 为什么不能用
readFileToString()随意处理图片? - [ ]
writeStringToFile()如何实现追加写入? - [ ] Commons IO 与 JDK
Files是什么关系? - [ ] 有 Commons IO 后为什么仍然要学习原生 IO?
- [ ] 为什么超大文件通常不应该整体读取成一个
String? - [ ] Commons IO 能否替代异常处理和业务校验?
7.2 代码阅读
阅读:
File source =
new File("a.jpg");
File target =
new File("backup/a.jpg");
FileUtils.copyFile(
source,
target
);
回答:
- [ ] 它复制的是字符还是字节?
- [ ] 图片是否需要指定 UTF-8?
- [ ] 为什么?
阅读:
String content =
FileUtils.readFileToString(
new File("article.txt"),
StandardCharsets.UTF_8
);
回答:
- [ ] 文件中的数据最开始是什么形式?
- [ ]
UTF_8在哪一步发挥作用? - [ ] 最终为什么得到
String? - [ ] 如果文件真实编码为 GBK,会发生什么?
7.3 手写代码
关闭 AI 自动补全完成:
- [ ] 引入 Commons IO Maven 依赖。
- [ ] 使用
FileUtils.copyFile()复制一个文件。 - [ ] 使用
FileUtils.copyDirectory()复制整个文件夹。 - [ ] 使用
FileUtils.deleteDirectory()删除测试目录。 - [ ] 使用
readFileToString()读取 UTF-8 文本。 - [ ] 使用
writeStringToFile()写 UTF-8 文本。 - [ ] 使用追加模式向日志文件增加数据。
- [ ] 使用
readLines()将文本转换成List<String>。 - [ ] 使用原生字节流重新实现一次
copyFile()的核心功能。
最后比较:
自己实现
vs
Commons IO
各自的职责和价值。
7.4 Debug
下面程序:
String content =
FileUtils.readFileToString(
new File("image.jpg"),
StandardCharsets.UTF_8
);
System.out.println(content);
要求:
- [ ] 代码调用形式是否成立?
- [ ] 业务逻辑为什么错误?
- [ ] 图片应该使用什么数据模型处理?
- [ ] 修正设计。
下面代码:
FileUtils.deleteDirectory(
new File(userInput)
);
要求:
- [ ] 找出潜在风险。
- [ ] 为什么不能直接信任用户输入路径?
- [ ] 删除前应该验证哪些条件?
- [ ] 设计一个更安全的删除流程。
7.5 综合训练
完成一个:
Java 文件备份中心
需求:
用户输入源目录
↓
验证目录
↓
扫描文件
↓
显示文件数量与大小
↓
备份到 backup
↓
找出所有 .txt/.md/.java
↓
统计文本总行数
↓
生成 backup-report.txt
要求至少使用:
File
递归
Commons IO
BufferedReader
PrintWriter
Charset
try-with-resources
在编码前必须先完成:
- 需求拆分。
- 数据流设计。
- 方法划分。
- 文件结构设计。
- 伪代码。
禁止直接把所有代码塞进:
main()
7.6 本章验收
不查看资料,能够先完整画出:
Java File 与 IO
│
├── File / Path
│ └── 文件系统对象
│
├── 字节流
│ ├── InputStream
│ └── OutputStream
│
├── 字符流
│ ├── Reader
│ └── Writer
│
├── 缓冲流
│ ├── BufferedInputStream
│ ├── BufferedOutputStream
│ ├── BufferedReader
│ └── BufferedWriter
│
├── 转换流
│ ├── InputStreamReader
│ └── OutputStreamWriter
│
├── 打印流
│ ├── PrintStream
│ └── PrintWriter
│
├── 数据流
│ ├── DataInputStream
│ └── DataOutputStream
│
├── 资源管理
│ └── try-with-resources
│
└── 工具 API
├── Files
└── Commons IO / FileUtils
然后面对一个需求:
将一个包含图片、Markdown、Java 源码和普通文本的目录完整备份,并统计其中所有文本文件的总行数。
能够主动完成:
分析数据类型
↓
选择 API
↓
设计递归
↓
确定字符集
↓
设计资源释放
↓
编码实现
↓
异常处理
而不是看到文件问题就随机尝试:
FileReader?
FileInputStream?
BufferedReader?
FileUtils?
最后应该能够回答下面这个问题:
面对任何一个基础文件 IO 需求,我为什么选择这个流或这个 API,而不是另外一个?
如果能够回答并独立实现,那么 GROUP 07 · File 与 IO 的核心学习目标就真正完成了。