🚀 Java 巩固进阶 · 第13天
主题:File 类 + 路径操作 ------ IO 体系的第一块基石
📅 进度概览 :从今天起,我们正式进入 Java IO 流体系 。第一站:
java.io.File。💡 核心价值:
- 文件操作基石 :
File是操作文件/目录元数据的唯一入口,后续所有流(Stream)都依赖它定位资源。- SpringBoot 实战必备 :文件上传存储、配置文件加载、日志目录初始化,处处可见
File的身影。- 路径兼容性:掌握跨平台路径写法,避免 "在我机器上能跑,上线就报错" 的经典坑。
- 递归思维训练:文件遍历是理解递归的最佳场景,也是面试高频考点。
一、File 类本质:它到底能做什么?
1. 核心定位
┌─────────────────────────────────┐
│ 📁 File 类 │
├─────────────────────────────────┤
│ ✅ 表示文件/目录的"路径名" │
│ ✅ 操作元数据:创建、删除、重命名│
│ ✅ 判断属性:是否存在、是文件还是目录│
│ ❌ 不负责读写文件内容! │
│ (读写内容 → 交给 IO 流) │
└─────────────────────────────────┘
2. 路径分隔符:跨平台第一坑 ⚠️
| 系统 | 分隔符 | 错误示例 | 正确写法 |
|---|---|---|---|
| Windows | \ |
new File("D:\test\a.txt") |
new File("D:/test/a.txt") |
| Linux/macOS | / |
new File("/home/user/test") |
✅ 原生支持 |
| 通用方案 | File.separator |
- | new File("data" + File.separator + "a.txt") |
💡 最佳实践:
- 代码中优先使用
/,Java 在 Windows 上也能自动识别- 动态拼接路径时,使用
File.separator或Paths.get()(NIO)
二、File 构造方法:三种姿势,推荐一种
java
// ❌ 姿势1:直接传完整路径(硬编码,不灵活)
File file1 = new File("D:/workspace/project/data/a.txt");
// ✅ 姿势2:父路径 + 子路径(最推荐!解耦、易维护)
File dir = new File("data");
File file2 = new File(dir, "a.txt"); // → data/a.txt
// ✅ 姿势3:字符串数组(适合动态拼接多级路径)
File file3 = new File("data", "sub", "a.txt"); // → data/sub/a.txt
🔍 为什么推荐姿势2?
- 路径可配置:
dir可从application.yml读取- 便于单元测试:轻松替换临时目录
- 避免路径拼接错误:
new File(dir, name)自动处理分隔符
三、核心判断方法(必背清单 ✅)
java
File file = new File("data/a.txt");
// 🔍 存在性与类型判断
file.exists(); // [关键] 文件/目录是否存在?
file.isFile(); // 是否是普通文件?
file.isDirectory(); // 是否是目录?
// 📋 基本信息获取
file.getName(); // "a.txt"(最后一部分)
file.getPath(); // "data/a.txt"(构造时的路径)
file.getAbsolutePath(); // "/Users/xxx/project/data/a.txt"(完整绝对路径)
file.length(); // 文件大小(字节),目录返回 0
file.lastModified(); // 最后修改时间戳(毫秒)
// 🔄 转换(后续与 NIO 互操作有用)
file.toURI(); // 转为 URI
file.toPath(); // 转为 Path(Java 7+ NIO)
⚠️ 高频陷阱:
javaFile file = new File("a.txt"); System.out.println(file.length()); // 如果文件不存在,返回 0!不是异常! // ✅ 正确做法:先判断 exists() && isFile()
四、创建 & 删除:安全操作三板斧
1. 创建文件(注意异常处理)
java
File file = new File("data/a.txt");
// ✅ 标准写法:先判断 + 捕获异常
if (!file.exists()) {
// 确保父目录存在!否则 createNewFile 会静默失败
File parent = file.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs(); // 创建父目录
}
try {
boolean created = file.createNewFile(); // 原子操作,线程安全
System.out.println("创建成功:" + created);
} catch (IOException e) {
e.printStackTrace(); // 生产环境建议用日志框架
}
}
2. 创建目录:mkdir() vs mkdirs()
java
File dir1 = new File("single");
dir1.mkdir(); // 仅创建单层,父目录不存在则失败
File dir2 = new File("a/b/c");
dir2.mkdirs(); // ✅ 递归创建多级目录(推荐!永远优先用这个)
3. 删除操作:注意"空目录"限制
java
// 删除文件
file.delete(); // 成功返回 true
// 删除目录:必须是空目录!
File emptyDir = new File("data/empty");
emptyDir.delete(); // ✅ 成功
// ❌ 非空目录直接 delete() 会失败!需要递归删除(见下方实战任务)
💡 SpringBoot 实践 :
文件上传时,先检查存储目录是否存在,不存在则
mkdirs()初始化:
java@Value("${file.upload-dir}") private String uploadDir; @PostConstruct public void init() { File dir = new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); // 应用启动时自动创建上传目录 } }
五、遍历文件夹:listFiles() 的正确打开方式
java
File dir = new File("data");
// ⚠️ 关键:listFiles() 可能返回 null!(目录不存在/无权限)
File[] files = dir.listFiles();
if (files == null) {
System.err.println("无法访问目录:" + dir.getAbsolutePath());
return;
}
// 遍历打印
for (File f : files) {
String type = f.isDirectory() ? "[DIR] " : "[FILE]";
System.out.println(type + f.getName() + " (" + f.length() + " bytes)");
}
🔥 进阶:文件名过滤器(只查 .txt 文件)
java
// 方式1:匿名内部类
File[] txtFiles = dir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".txt");
}
});
// 方式2:Lambda 表达式(Java 8+,更简洁)
File[] txtFiles = dir.listFiles((d, name) -> name.endsWith(".txt"));
// 方式3:File 过滤器(可结合文件属性)
File[] largeFiles = dir.listFiles(file -> file.isFile() && file.length() > 1024 * 1024);
六、递归遍历:文件扫描的万能钥匙 🔑
java
/**
* 递归打印目录下所有文件(含子目录)
* @param dir 起始目录
* @param indent 缩进层级(用于可视化树形结构)
*/
public static void listAll(File dir, String indent) {
// 1. 参数校验(防御式编程)
if (dir == null || !dir.exists() || !dir.isDirectory()) {
return;
}
// 2. 获取子项(注意 null 检查!)
File[] files = dir.listFiles();
if (files == null) return;
// 3. 遍历处理
for (File f : files) {
System.out.println(indent + "├─ " + f.getName() +
(f.isDirectory() ? "/" : "") +
(f.isFile() ? " (" + f.length() + "B)" : ""));
// 4. 递归:如果是目录,深入一层
if (f.isDirectory()) {
listAll(f, indent + "│ "); // 缩进增加
}
}
}
// 调用示例
listAll(new File("data"), "");
输出效果:
├─ config/
│ ├─ application.yml (2.1KB)
│ └─ logback.xml (1.5KB)
├─ upload/
│ ├─ image.png (3.2MB)
│ └─ doc.pdf (1.1MB)
└─ readme.md (4.2KB)
💡 递归三要素(面试必问):
- 终止条件 :
!dir.isDirectory()或files == null- 单层逻辑:遍历当前目录所有文件/子目录
- 递归调用:遇到子目录时,调用自身处理
七、🎯 今日实战任务:文件管理小工具
任务1:初始化项目目录结构
java
// 要求:在当前项目下创建多层目录 data/upload/{image, doc, temp}
// 提示:使用 mkdirs() + 数组遍历
String[] subDirs = {"image", "doc", "temp"};
File baseDir = new File("data/upload");
// TODO: 补全代码...
任务2:实现"安全删除"工具方法
java
/**
* 递归删除文件或目录(无论是否为空)
* @param file 要删除的文件/目录
* @return 是否全部删除成功
*/
public static boolean deleteRecursively(File file) {
// TODO: 实现逻辑
// 提示:
// 1. 如果是目录,先递归删除所有子项
// 2. 再删除自身
// 3. 注意异常处理和返回值
}
任务3:统计目录信息(综合练习)
java
/**
* 统计目录下:文件总数、总大小、最大文件
*/
public static class DirStat {
int fileCount;
long totalSize;
File largestFile;
// TODO: 添加构造方法/getter
}
public static DirStat analyzeDir(File dir) {
// TODO: 递归遍历 + 数据统计
// 挑战:跳过隐藏文件(file.isHidden())
}
任务4:结合 SpringBoot 配置
yaml
# application.yml
app:
upload:
base-dir: ./uploads
allowed-extensions: [jpg, png, pdf]
java
// 要求:读取配置,初始化目录,并实现文件扩展名过滤的 listFiles
@Value("${app.upload.allowed-extensions}")
private List<String> allowedExts;
private boolean isAllowed(String filename) {
// TODO: 判断文件名后缀是否在允许列表中(忽略大小写)
}
📝 第13天 · 核心总结
-
File 类定位:
- 操作路径/元数据,不读写内容
- 所有流(FileInputStream 等)的"入口"
-
路径兼容性:
- 优先用
/或File.separator - 构造推荐:
new File(parent, child)
- 优先用
-
安全操作守则:
- 创建文件前:
getParentFile().mkdirs() - 创建目录:永远用
mkdirs() - 遍历目录:
listFiles()必须判null - 删除目录:非空需递归
- 创建文件前:
-
递归遍历模板(背下来!):
javaif (dir == null || !dir.isDirectory()) return; File[] files = dir.listFiles(); if (files == null) return; for (File f : files) { // 处理当前文件 if (f.isDirectory()) listAll(f, indent + " "); // 递归 } -
SpringBoot 实践点:
- 应用启动时初始化上传/日志目录
- 配置文件管理路径,避免硬编码
- 文件上传前校验扩展名、大小