File 常用 API 与目录遍历 | JavaSE
File 常用 API 与目录遍历
一、学习目标
学完本章,你应该能够:
- 能够使用
exists()、isFile()、isDirectory()判断一个路径对应的文件系统对象状态。 - 能够使用
getName()、length()、lastModified()、getPath()、getAbsolutePath()获取文件基本信息。 - 能够使用
createNewFile()、mkdir()、mkdirs()、delete()完成基本文件系统操作。 - 能够解释
mkdir()与mkdirs()、list()与listFiles()的区别。 - 能够使用
listFiles()遍历目录下的一级文件和子目录。 - 能够正确处理
listFiles()返回null、空数组等边界情况。 - 能够编写一个简单的目录统计程序,统计某目录的一级文件数和一级子目录数。
二、核心知识
2.1 File API 可以分成三组
上一章我们已经明确:
File 对象
↓
表示一个抽象路径
但只有路径还不够。
实际开发中,我们通常还需要回答:
这个路径存在吗?
它是文件还是目录?
这个文件叫什么?
有多大?
最后什么时候修改?
能不能创建?
能不能删除?
目录下面有什么?
因此 File 的常用 API 可以大致分成三组:
File
│
├── 判断与获取
│
├── 创建与删除
│
└── 目录遍历
这是本章的主线。
2.2 判断与获取类 API
原课程中的核心方法如下:
| 方法 | 作用 |
| ------------------- | -------------------------------- |
| exists() | 判断路径是否存在 |
| isFile() | 判断是否为普通文件 |
| isDirectory() | 判断是否为目录 |
| getName() | 获取文件或目录名称 |
| length() | 获取文件长度,单位字节 |
| lastModified() | 获取最后修改时间 |
| getPath() | 获取 File 创建时对应的路径字符串 |
| getAbsolutePath() | 获取绝对路径 |
最重要的一组通常是:
file.exists();
file.isFile();
file.isDirectory();
因为后续很多操作都建立在:
先判断对象是什么,再决定怎么处理。
例如:
File
↓
存在吗?
↓
文件?
↓
目录?
2.3 exists()
语法:
boolean exists()
作用:
判断当前 File 所表示的文件或目录是否真实存在。
例如:
File file = new File("data/test.txt");
System.out.println(file.exists());
如果:
data/test.txt
真实存在:
true
否则:
false
这也再次证明上一章的重要结论:
File 对象存在
≠
磁盘文件存在
因为:
File file = new File("not-exist.txt");
对象完全可以创建成功。
但是:
file.exists();
可能得到:
false
2.4 isFile()
语法:
boolean isFile()
用于判断:
当前路径是否表示一个普通文件。
例如:
File file = new File("data/user.txt");
System.out.println(file.isFile());
如果 user.txt 是真实存在的普通文件:
true
2.5 isDirectory()
语法:
boolean isDirectory()
用于判断:
当前路径是否表示目录。
例如:
File directory = new File("data");
System.out.println(directory.isDirectory());
如果 data 是目录:
true
2.6 三个判断方法通常组合使用
实际开发中不会孤立地背 API,而往往这样写:
File file = new File("data");
if (!file.exists()) {
System.out.println("路径不存在");
} else if (file.isFile()) {
System.out.println("这是文件");
} else if (file.isDirectory()) {
System.out.println("这是目录");
}
思维模型:
File
│
▼
exists()
│
├── false → 不存在
│
└── true
│
├── isFile() → 普通文件
│
└── isDirectory() → 目录
这套判断结构以后会大量出现。
2.7 getName()
语法:
String getName()
例如:
File file =
new File("D:/Java/data/student.txt");
System.out.println(file.getName());
得到:
student.txt
如果是:
File directory =
new File("D:/Java/data");
System.out.println(directory.getName());
通常得到:
data
因此:
getName()获取的是当前路径最后一级的名称。
2.8 getPath()
语法:
String getPath()
它返回:
当前 File 对象所保存的路径形式。
例如:
File file =
new File("data/student.txt");
System.out.println(file.getPath());
得到的通常仍然是:
data\student.txt
或者对应平台形式。
它可能仍然是:
相对路径
2.9 getAbsolutePath()
语法:
String getAbsolutePath()
作用:
获取该 File 对象对应的绝对路径。
例如:
File file =
new File("data/student.txt");
System.out.println(file.getAbsolutePath());
假设:
user.dir
=
D:\JavaStudy
那么可能得到:
D:\JavaStudy\data\student.txt
因此需要区分:
getPath()
↓
保存的路径形式
getAbsolutePath()
↓
转换后的绝对路径
2.10 length()
语法:
long length()
用于获得:
普通文件的长度,单位为字节(byte)。
例如文件大小为:
1024 bytes
那么:
File file = new File("data.txt");
System.out.println(file.length());
可能得到:
1024
注意返回类型:
long
因为文件可能非常大。
2.11 length() 最重要的边界
对于:
普通文件
可以用:
length()
获取文件大小。
但是不要简单认为:
directory.length()
等于:
目录下所有文件大小之和。
不是。
对于目录,File.length() 的返回值没有这种业务含义。
因此以后如果要:
统计整个文件夹占用空间
需要:
遍历目录
+
递归
+
累加文件 length()
而不是:
directory.length();
这将在后续综合应用中出现。
2.12 lastModified()
语法:
long lastModified()
用于获得:
文件最后一次修改的时间。
返回:
long
表示从:
1970-01-01 00:00:00 UTC
附近的 Unix Epoch 起计算的毫秒时间值。
例如:
File file = new File("data.txt");
System.out.println(file.lastModified());
可能得到:
1788393600000
这种数字不适合直接给用户看。
因此实际项目通常需要:
时间戳
↓
日期时间对象
↓
格式化字符串
本章只要求知道:
file.lastModified()
可以获得文件的最后修改时间。
三、使用方法
3.1 综合获取文件信息
例如:
import java.io.File;
public class FileInfoDemo {
public static void main(String[] args) {
File file =
new File("data/test.txt");
System.out.println(
"是否存在:" + file.exists()
);
System.out.println(
"是否文件:" + file.isFile()
);
System.out.println(
"是否目录:" + file.isDirectory()
);
System.out.println(
"名称:" + file.getName()
);
System.out.println(
"长度:" + file.length()
);
System.out.println(
"最后修改时间:" + file.lastModified()
);
System.out.println(
"原路径:" + file.getPath()
);
System.out.println(
"绝对路径:" + file.getAbsolutePath()
);
}
}
实际项目中:
先获得 File
↓
判断存在性
↓
判断类型
↓
读取其他属性
通常更合理。
3.2 createNewFile()
语法:
boolean createNewFile()
throws IOException
作用:
如果目标文件不存在,就创建一个新的空文件。
例如:
File file =
new File("data/test.txt");
boolean result =
file.createNewFile();
System.out.println(result);
可能得到:
true
表示:
文件原先不存在,这一次成功创建。
如果再次执行:
file.createNewFile();
通常得到:
false
表示:
文件已经存在,因此没有再次创建。
因此返回值可以理解为:
true
→ 这次真的创建成功了
false
→ 没有创建新的文件
3.3 createNewFile() 创建的是空文件
例如:
new File("hello.txt").createNewFile();
创建的是:
hello.txt
但里面不会自动有:
Hello Java
因为:
File
负责文件系统操作
真正写入内容仍然需要:
IO 流
3.4 父目录不存在怎么办
假设:
data
目录不存在。
直接:
File file =
new File("data/user/test.txt");
file.createNewFile();
不能简单理解为:
Java 会自动创建 data
Java 会自动创建 user
然后再创建 test.txt
createNewFile() 不负责自动补齐父目录。
正确思维是:
先保证父目录存在
↓
再创建文件
例如:
File directory =
new File("data/user");
directory.mkdirs();
File file =
new File(directory, "test.txt");
file.createNewFile();
3.5 mkdir()
语法:
boolean mkdir()
作用:
创建当前路径表示的一个目录。
例如:
File directory =
new File("data");
System.out.println(
directory.mkdir()
);
如果成功:
true
3.6 为什么教学中说 mkdir() 创建“一级目录”
原课程常记忆为:
mkdir()
只能创建一级文件夹
更准确地理解应该是:
mkdir()只创建当前 File 所表示的那个目录,并不会自动创建缺失的父目录。
例如:
data
不存在
直接创建:
File dir =
new File("data/a/b");
dir.mkdir();
由于:
data
data/a
都还不存在,因此通常失败。
它不是:
路径最多只能包含一层
而是:
父目录必须已经存在
这是理解 mkdir() 的关键。
3.7 mkdirs()
语法:
boolean mkdirs()
作用:
创建目标目录,并且可以同时创建不存在的父目录。
例如:
File directory =
new File("data/user/avatar");
System.out.println(
directory.mkdirs()
);
如果:
data
user
avatar
都不存在,则 Java 可以逐级创建:
data
└── user
└── avatar
因此最常见记忆:
mkdir()
单级创建
mkdirs()
多级创建
但真正应该理解为:
mkdir()
只尝试创建当前目录
mkdirs()
必要时连不存在的父目录一起创建
3.8 mkdir() 与 mkdirs() 对比
| 方法 | 是否自动补齐父目录 |
| ---------- | ------------------ |
| mkdir() | 否 |
| mkdirs() | 是 |
例如:
a/b/c
而 a、b 都不存在。
new File("a/b/c").mkdir();
通常:
失败
而:
new File("a/b/c").mkdirs();
可以:
创建 a
创建 b
创建 c
3.9 delete()
语法:
boolean delete()
用于删除:
普通文件
或
空目录
例如:
File file =
new File("data/test.txt");
System.out.println(
file.delete()
);
如果删除成功:
true
失败:
false
3.10 delete() 删除目录的重要限制
假设:
data
└── user.txt
然后:
File directory =
new File("data");
directory.delete();
不能直接删除整个目录树。
因为:
使用
File.delete()删除目录时,该目录必须为空。
所以:
空目录
→ 可以删除
非空目录
→ 不能直接 delete()
如果以后想删除:
data
├── a.txt
├── b.txt
└── child
需要:
先处理内部内容
↓
再删除目录
这正是递归可以发挥作用的地方。
3.11 删除不是“回收站操作”
在文件管理器里:
Delete
有时会进入回收站。
但程序中的:
file.delete();
不要把它理解成:
安全移动到系统回收站。
因此真实开发中:
删除操作必须非常谨慎
尤其不要:
new File("D:/").delete();
或者对未知路径执行批量删除逻辑。
四、原理与进阶
4.1 File 方法其实在和操作系统文件系统通信
上一章:
new File(...)
主要只是构造抽象路径。
但是:
exists()
isFile()
length()
createNewFile()
delete()
listFiles()
这些方法需要知道:
磁盘的真实状态
因此它们需要与:
操作系统
+
文件系统
发生交互。
可以理解:
Java File 对象
│
│ API 调用
▼
JDK 文件系统实现
│
▼
操作系统文件系统
│
▼
真实磁盘对象
因此文件操作可能受到:
文件是否存在
权限
文件是否被其他程序占用
操作系统
文件系统
I/O 错误
影响。
不能把文件操作理解成普通内存变量赋值那么简单。
4.2 list():获得一级名称
语法:
String[] list()
假设:
data
├── a.txt
├── b.txt
└── images
执行:
File dir =
new File("data");
String[] names =
dir.list();
可能得到:
a.txt
b.txt
images
注意:
String[]
里面只有:
当前目录下一级成员的名称。
不是完整 File 对象。
4.3 listFiles():获得一级 File 对象
语法:
File[] listFiles()
例如:
File dir =
new File("data");
File[] files =
dir.listFiles();
获得的是:
File[]
其中每个元素都代表:
data/a.txt
data/b.txt
data/images
这样的文件系统对象。
因此相比:
list()
实际开发中通常:
listFiles()
更重要。
4.4 list() 与 listFiles() 的核心区别
list()
↓
String[]
↓
只得到名字
而:
listFiles()
↓
File[]
↓
得到 File 对象
↓
可以继续 isFile()
isDirectory()
length()
getName()
...
因此如果只是:
看看有哪些名字
可以:
list()
如果后面还要继续处理这些对象:
判断
统计
删除
递归
搜索
应该重点掌握:
listFiles()
4.5 listFiles() 只能直接遍历一级
假设:
data
├── a.txt
├── image
│ ├── 1.png
│ └── 2.png
└── doc
└── java
└── java.md
执行:
File[] files =
new File("data").listFiles();
只会获得:
data/a.txt
data/image
data/doc
不会自动获得:
data/image/1.png
data/image/2.png
data/doc/java/java.md
所以必须记住:
listFiles()是一级目录遍历。
那么怎样进入:
目录中的目录
目录中的目录中的目录
呢?
答案就是下一章:
递归
4.6 一级目录遍历
基础写法:
import java.io.File;
public class FileListDemo {
public static void main(String[] args) {
File directory =
new File("data");
File[] files =
directory.listFiles();
if (files != null) {
for (File file : files) {
System.out.println(
file.getAbsolutePath()
);
}
}
}
}
这里:
File[]
↓
增强 for
↓
一个个 File
这其实与前面集合遍历思维完全一致。
4.7 为什么一定要判断 files != null
这是 File 学习中的重要 Debug 点。
错误写法:
File[] files =
directory.listFiles();
for (File file : files) {
System.out.println(file);
}
如果:
files == null
那么:
增强 for
↓
尝试遍历 null
↓
NullPointerException
因此应该:
if (files == null) {
return;
}
for (File file : files) {
...
}
或者:
if (files != null) {
for (File file : files) {
...
}
}
4.8 listFiles() 为什么可能返回 null
最重要的情况包括:
当前 File 不是目录
或者:
发生 I/O 错误
例如:
File file =
new File("data.txt");
File[] files =
file.listFiles();
如果:
data.txt
是普通文件:
files == null
是完全可能的。
注意另一个情况:
目录真实存在
但是里面没有任何内容
此时概念上应该得到:
长度为 0 的 File[]
而不是因为“空目录”就必然返回 null。
因此:
null
≠
空目录
这是重要区别。
4.9 listFiles() 返回顺序不能作为业务依据
不要写出:
第一个一定是 a.txt
第二个一定是 b.txt
第三个一定是 c.txt
然后依赖这个顺序执行业务逻辑。
文件系统遍历结果:
不保证按照固定的字母顺序排列。
如果业务要求排序:
listFiles()
↓
得到数组
↓
自己排序
五、实践应用
5.1 统计一级文件和目录数量
假设:
data
├── a.txt
├── b.txt
├── image
└── doc
我们想统计:
普通文件:2
目录:2
可以:
import java.io.File;
public class DirectoryStatistics {
public static void main(String[] args) {
File directory =
new File("data");
File[] files =
directory.listFiles();
if (files == null) {
System.out.println(
"无法遍历该目录"
);
return;
}
int fileCount = 0;
int directoryCount = 0;
for (File file : files) {
if (file.isFile()) {
fileCount++;
} else if (file.isDirectory()) {
directoryCount++;
}
}
System.out.println(
"一级文件数量:" + fileCount
);
System.out.println(
"一级目录数量:" + directoryCount
);
}
}
这里已经组合使用:
listFiles()
+
for
+
isFile()
+
isDirectory()
这就是 JavaSE 学习逐渐进入综合阶段的表现。
5.2 文件信息查看器
可以封装:
public static void printFileInfo(File file) {
if (!file.exists()) {
System.out.println("路径不存在");
return;
}
System.out.println(
"名称:" + file.getName()
);
System.out.println(
"绝对路径:" +
file.getAbsolutePath()
);
if (file.isFile()) {
System.out.println("类型:文件");
System.out.println(
"大小:" +
file.length() +
" bytes"
);
} else if (file.isDirectory()) {
System.out.println("类型:目录");
}
}
此时已经出现非常典型的工程思维:
输入 File
↓
合法性检查
↓
分类
↓
分别处理
六、常见问题
6.1 exists() 为 false,为什么 File 对象明明创建成功?
因为:
new File(...)
创建的是:
Java File 对象
而:
exists()
检查的是:
文件系统中的真实目标
两者不是一个层次。
6.2 isFile() 为 false 就意味着它一定是目录吗?
不能。
因为还可能:
路径根本不存在
因此更稳妥:
if (!file.exists()) {
...
} else if (file.isFile()) {
...
} else if (file.isDirectory()) {
...
}
6.3 length() 能统计文件夹大小吗?
不能把它这样使用。
如果需要统计整个目录树大小:
遍历所有文件
+
递归进入子目录
+
累加每个文件 length()
6.4 mkdir() 和 mkdirs() 到底怎么记?
不要只死记:
一个一级
一个多级
应该理解:
mkdir()
只创建当前目标目录,
父目录必须已经存在
mkdirs()
缺失的父目录可以一起创建
6.5 delete() 为什么删除文件夹失败?
很可能:
目录非空
File.delete() 对目录要求:
必须为空
6.6 为什么 listFiles() 会空指针?
因为你可能:
File[] files =
file.listFiles();
得到:
null
然后直接:
for (File f : files)
导致:
NullPointerException
所以文件系统代码必须特别注意:
null 边界
6.7 空目录和 listFiles() == null 一回事吗?
不是。
空目录
→ 通常是长度为 0 的数组
不是目录 / I/O 错误
→ 可能返回 null
6.8 listFiles() 能一次获得整个目录树吗?
不能。
它只能:
获取当前目录一级成员
更深的目录需要:
继续对那个子目录调用 listFiles()
而这种:
同一种操作不断进入下一层
就是下一章递归的重要应用。
七、练习与验收
7.1 知识问答
请闭卷回答:
exists()、isFile()、isDirectory()分别解决什么问题?- 为什么
isFile() == false不代表一定是目录? getPath()和getAbsolutePath()有什么区别?length()返回什么单位?length()是否可以直接表示一个目录全部内容大小?createNewFile()返回true和false分别说明什么?createNewFile()是否会自动创建缺失的父目录?mkdir()与mkdirs()的真正区别是什么?- 为什么
delete()不能直接删除非空目录? list()与listFiles()的返回类型分别是什么?- 为什么实际开发中通常更加关注
listFiles()? listFiles()是一级遍历还是递归遍历?listFiles()什么情况下可能返回null?- 空目录调用
listFiles()是否意味着一定返回null? listFiles()返回的对象顺序是否应该作为业务逻辑依据?
7.2 代码阅读
不要运行:
import java.io.File;
public class FileReadDemo {
public static void main(String[] args) {
File dir =
new File("data");
File[] files =
dir.listFiles();
if (files == null) {
System.out.println("无法遍历");
return;
}
int fileCount = 0;
int dirCount = 0;
for (File file : files) {
if (file.isFile()) {
fileCount++;
}
if (file.isDirectory()) {
dirCount++;
}
}
System.out.println(fileCount);
System.out.println(dirCount);
}
}
回答:
- 这段代码统计的是整个目录树还是一级目录?
- 为什么需要判断
files == null? - 如果
data是一个空目录,两个计数应该是多少? - 如果
data本身是一个普通文件,listFiles()可能得到什么? - 这里为什么使用
File[]而不是String[]?
7.3 手写代码
任务一:文件信息查看器
输入一个路径,输出:
是否存在
是否文件
是否目录
名称
绝对路径
文件大小(如果是文件)
最后修改时间
要求:
先判断,再获取对应属性。
任务二:目录创建
创建:
data
└── users
└── avatar
要求分别实验:
mkdir()
和:
mkdirs()
观察二者区别。
任务三:创建文件
创建:
data/users/user.txt
要求:
- 先确保父目录存在;
- 再创建文件;
- 输出
createNewFile()返回值; - 重复运行一次,观察结果变化。
任务四:一级目录遍历
遍历指定目录,依次输出:
[FILE] a.txt
[DIR ] images
[FILE] data.csv
只处理:
一级成员。
任务五:统计
统计一个目录:
一级普通文件数量
一级目录数量
一级普通文件总字节数
暂时不要递归进入子目录。
7.4 Debug
观察:
public static void printDirectory(File file) {
File[] files =
file.listFiles();
for (File child : files) {
System.out.println(child);
}
}
分析:
- 什么情况下会发生空指针?
- 如何修复?
- 是否应该先检查
exists()? - 是否应该先检查
isDirectory()? - 最终写出一个健壮版本。
7.5 综合训练
编写:
public static void analyseDirectory(File directory)
要求:
- 路径不存在时输出提示;
- 参数是普通文件时输出提示;
- 参数是目录时开始遍历;
- 输出所有一级成员名称;
- 区分
[FILE]与[DIR]; - 统计一级文件数;
- 统计一级目录数;
- 统计一级普通文件总大小;
- 正确处理
listFiles() == null。
注意:
本题禁止递归。
因为递归属于下一章。
7.6 本章验收
如果你能闭卷口述下面这条链路,本章核心就已经掌握:
File 对象
↓
exists()
确认真实路径是否存在
↓
isFile() / isDirectory()
判断目标类型
↓
getName()
length()
lastModified()
getPath()
getAbsolutePath()
获得信息
↓
createNewFile()
mkdir()
mkdirs()
delete()
操作文件系统
↓
listFiles()
获得一级 File 对象
↓
遍历、判断、统计
最终验收:
- [ ] 闭卷写出判断类 API。
- [ ] 闭卷写出获取类 API。
- [ ] 闭卷写出创建与删除类 API。
- [ ] 能解释
mkdir()与mkdirs()。 - [ ] 能解释
list()与listFiles()。 - [ ] 能处理
listFiles() == null。 - [ ] 能独立完成一级目录遍历。
- [ ] 能统计一级文件和目录。
- [ ] 不把一级遍历误认为递归遍历。