File 常用 API 与目录遍历 | JavaSE

File 常用 API 与目录遍历

一、学习目标

学完本章,你应该能够:

  1. 能够使用 exists()isFile()isDirectory() 判断一个路径对应的文件系统对象状态。
  2. 能够使用 getName()length()lastModified()getPath()getAbsolutePath() 获取文件基本信息。
  3. 能够使用 createNewFile()mkdir()mkdirs()delete() 完成基本文件系统操作。
  4. 能够解释 mkdir()mkdirs()list()listFiles() 的区别。
  5. 能够使用 listFiles() 遍历目录下的一级文件和子目录。
  6. 能够正确处理 listFiles() 返回 null、空数组等边界情况。
  7. 能够编写一个简单的目录统计程序,统计某目录的一级文件数和一级子目录数。

二、核心知识

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

ab 都不存在。

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 知识问答

请闭卷回答:

  1. exists()isFile()isDirectory() 分别解决什么问题?
  2. 为什么 isFile() == false 不代表一定是目录?
  3. getPath()getAbsolutePath() 有什么区别?
  4. length() 返回什么单位?
  5. length() 是否可以直接表示一个目录全部内容大小?
  6. createNewFile() 返回 truefalse 分别说明什么?
  7. createNewFile() 是否会自动创建缺失的父目录?
  8. mkdir()mkdirs() 的真正区别是什么?
  9. 为什么 delete() 不能直接删除非空目录?
  10. list()listFiles() 的返回类型分别是什么?
  11. 为什么实际开发中通常更加关注 listFiles()
  12. listFiles() 是一级遍历还是递归遍历?
  13. listFiles() 什么情况下可能返回 null
  14. 空目录调用 listFiles() 是否意味着一定返回 null
  15. 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);
    }
}

回答:

  1. 这段代码统计的是整个目录树还是一级目录?
  2. 为什么需要判断 files == null
  3. 如果 data 是一个空目录,两个计数应该是多少?
  4. 如果 data 本身是一个普通文件,listFiles() 可能得到什么?
  5. 这里为什么使用 File[] 而不是 String[]

7.3 手写代码

任务一:文件信息查看器

输入一个路径,输出:

是否存在
是否文件
是否目录
名称
绝对路径
文件大小(如果是文件)
最后修改时间

要求:

先判断,再获取对应属性。


任务二:目录创建

创建:

data
└── users
    └── avatar

要求分别实验:

mkdir()

和:

mkdirs()

观察二者区别。


任务三:创建文件

创建:

data/users/user.txt

要求:

  1. 先确保父目录存在;
  2. 再创建文件;
  3. 输出 createNewFile() 返回值;
  4. 重复运行一次,观察结果变化。

任务四:一级目录遍历

遍历指定目录,依次输出:

[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);
    }
}

分析:

  1. 什么情况下会发生空指针?
  2. 如何修复?
  3. 是否应该先检查 exists()
  4. 是否应该先检查 isDirectory()
  5. 最终写出一个健壮版本。

7.5 综合训练

编写:

public static void analyseDirectory(File directory)

要求:

  1. 路径不存在时输出提示;
  2. 参数是普通文件时输出提示;
  3. 参数是目录时开始遍历;
  4. 输出所有一级成员名称;
  5. 区分 [FILE][DIR]
  6. 统计一级文件数;
  7. 统计一级目录数;
  8. 统计一级普通文件总大小;
  9. 正确处理 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
  • [ ] 能独立完成一级目录遍历。
  • [ ] 能统计一级文件和目录。
  • [ ] 不把一级遍历误认为递归遍历。