你还在用 `File` 里那堆“祖传 API”硬写文件操作?都 2026 年了你不嫌累吗?
我先承认:我当年也用写过一堆“看起来能跑、但越看越心虚”的代码。什么返回 null 还得防、路径拼接靠手写斜杠……写到最后像在跟操作系统猜谜。后来我换到不是我变强了,是工具终于像个人了。😂这一篇我按你给的大纲来,从Path到Files文件搜索 + 增量备份工具(含过滤规则、跳过隐藏目录、保留目录结构、可选删除目标多余文件)。全程用人话讲清楚,也给你能直接抄走的代码。如果你觉得这篇文章对你有帮助,
👋 你好,欢迎来到我的博客!我是【菜鸟不学编程】
我是一个正在奋斗中的职场码农,步入职场多年,正在从“小码农”慢慢成长为有深度、有思考的技术人。在这条不断进阶的路上,我决定记录下自己的学习与成长过程,也希望通过博客结识更多志同道合的朋友。
🛠️ 主要方向包括 Java 基础、Spring 全家桶、数据库优化、项目实战等,也会分享一些踩坑经历与面试复盘,希望能为还在迷茫中的你提供一些参考。
💡 我相信:写作是一种思考的过程,分享是一种进步的方式。
如果你和我一样热爱技术、热爱成长,欢迎关注我,一起交流进步!
全文目录:
-
- 前言 😮💨
- I. `Path` 接口:路径不是字符串,别再拿它当拼积木😅
- II. `Files` 类:copy / move / delete / readAllBytes,这才叫“正经工具箱”🧰
- III. `DirectoryStream`:目录遍历 + 过滤(轻量、可控、不会一次性把你喂撑)📂
- IV. `FileVisitor`:递归遍历文件树(当你想“走遍全世界”的时候)🌲
- V. 属性与权限:`BasicFileAttributes` + `PosixFilePermissions`(别在权限坑里摔第二次)🔐
- VI. 示例:文件搜索 + 备份工具(能跑、可扩展、还不至于把你坑哭)🛠️
- 收尾:`java.nio.file` 的“正确打开方式”,其实就一句话
- 📝 写在最后
前言 😮💨
我先承认:我当年也用 java.io.File 写过一堆“看起来能跑、但越看越心虚”的代码。什么 new File("a/b")、listFiles() 返回 null 还得防、路径拼接靠手写斜杠……写到最后像在跟操作系统猜谜。
后来我换到 java.nio.file 才发现:不是我变强了,是工具终于像个人了。😂
这一篇我按你给的大纲来,从 Path 到 Files,从目录流遍历到文件树递归,再到权限属性,最后给一个能落地的完整例子:文件搜索 + 增量备份工具(含过滤规则、跳过隐藏目录、保留目录结构、可选删除目标多余文件)。全程用人话讲清楚,也给你能直接抄走的代码。
I. Path 接口:路径不是字符串,别再拿它当拼积木😅
Path 是对“文件路径”的抽象。它最讨喜的一点是:你不用自己操心路径分隔符到底是 / 还是 \。你只需要把“想去哪”说清楚,它帮你把路铺好。
1.1 创建 Path:别手搓斜杠了,真的
import java.nio.file.Path;
import java.nio.file.Paths;
Path p1 = Paths.get("data", "logs", "app.log"); // 自动适配系统分隔符
Path p2 = Path.of("data", "logs", "app.log"); // Java 11+ 推荐
1.2 常用操作:resolve / normalize / toAbsolutePath
Path base = Path.of("/tmp");
Path file = base.resolve("a").resolve("../b.txt"); // /tmp/a/../b.txt
Path normalized = file.normalize(); // /tmp/b.txt
Path abs = normalized.toAbsolutePath(); // 绝对路径(如果本来不是)
我特别爱 normalize(),因为它能把 .. 这类“看起来像 bug 的路径”理顺,日志里也更好看。
你要是线上排查问题时看到一串 ../../..,那感觉……就像有人拿麻绳在你脑子里打结。🤦
1.3 相对路径与 relativize():做备份/迁移时超实用
Path srcRoot = Path.of("/data/src");
Path srcFile = Path.of("/data/src/docs/readme.md");
Path relative = srcRoot.relativize(srcFile); // docs/readme.md
这一步在“保持目录结构复制”时会用到(后面示例会直接上)。
II. Files 类:copy / move / delete / readAllBytes,这才叫“正经工具箱”🧰
Files 是静态工具类,基本覆盖了你对文件操作的 80% 需求。关键点:它的异常更清晰,选项更明确,行为更可控。
2.1 copy:覆盖?保留属性?一眼就能看懂
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
Path from = Path.of("a.txt");
Path to = Path.of("backup", "a.txt");
Files.createDirectories(to.getParent());
Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
2.2 move:重命名 / 跨目录移动都能干
Files.move(
Path.of("tmp.txt"),
Path.of("done.txt"),
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE // 能原子就原子(不保证一定可用)
);
2.3 delete:deleteIfExists 比 exists + delete 更稳
Files.deleteIfExists(Path.of("old.log"));
这比你先 Files.exists() 再删更靠谱,因为你判断存在到删除之间可能被别人动过(典型的 TOCTOU 问题)。别小看这类“间隙”,线上就爱在这种地方搞事情。😑
2.4 readAllBytes:小文件一把梭,大文件别冲动
byte[] data = Files.readAllBytes(Path.of("small.bin"));
注意:这适合小文件。大文件请用 Files.newInputStream 或 Files.lines(流式处理),不然你会体验一次“内存瞬间蒸发”的魔法表演。🫥
III. DirectoryStream:目录遍历 + 过滤(轻量、可控、不会一次性把你喂撑)📂
Files.list() 返回 Stream 很香,但很多时候我更爱 DirectoryStream:它是 lazy 的,而且有“用完即关”的强制习惯,适合遍历大目录。
import java.nio.file.*;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(
Path.of("logs"),
entry -> Files.isRegularFile(entry) && entry.toString().endsWith(".log")
)) {
for (Path p : stream) {
System.out.println(p);
}
}
这里有个小细节:
DirectoryStream的过滤器是你自己写的DirectoryStream.Filter<Path>- 你可以做更复杂的过滤:跳过隐藏目录、跳过
node_modules、只要最近 7 天文件……都行
IV. FileVisitor:递归遍历文件树(当你想“走遍全世界”的时候)🌲
递归遍历目录树,最稳的方式之一就是 Files.walkFileTree + FileVisitor。
它的好处是:你能精确控制每个阶段的行为,比如遇到权限不足要不要跳过,遇到符号链接怎么处理。
4.1 一个“务实型”访问器:跳过隐藏目录 + 只处理普通文件
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
public class SimpleWalker extends SimpleFileVisitor<Path> {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (Files.isHidden(dir)) {
return FileVisitResult.SKIP_SUBTREE; // 跳过隐藏目录
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (attrs.isRegularFile()) {
System.out.println("file: " + file);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
// 访问失败别直接炸,打印一下继续(备份工具里很常见)
System.err.println("fail: " + file + " -> " + exc.getMessage());
return FileVisitResult.CONTINUE;
}
}
调用:
Files.walkFileTree(Path.of("data"), new SimpleWalker());
V. 属性与权限:BasicFileAttributes + PosixFilePermissions(别在权限坑里摔第二次)🔐
5.1 BasicFileAttributes:拿创建时间、最后修改时间、大小、类型
import java.nio.file.attribute.BasicFileAttributes;
Path p = Path.of("a.txt");
BasicFileAttributes attrs = Files.readAttributes(p, BasicFileAttributes.class);
System.out.println("size=" + attrs.size());
System.out.println("lastModified=" + attrs.lastModifiedTime());
System.out.println("isDirectory=" + attrs.isDirectory());
5.2 POSIX 权限:Linux/macOS 上能用,Windows 要谨慎
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Set;
Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwxr-x---");
Files.setPosixFilePermissions(Path.of("script.sh"), perms);
但我必须提醒一句:
PosixFilePermissions在非 POSIX 文件系统(比如 Windows 默认)可能不支持,抛UnsupportedOperationException。- 写跨平台工具时,要么做能力检测,要么优雅降级。
检测方式(简单点):
boolean posixSupported = FileSystems.getDefault()
.supportedFileAttributeViews()
.contains("posix");
VI. 示例:文件搜索 + 备份工具(能跑、可扩展、还不至于把你坑哭)🛠️
下面这个例子我写得比较“工程化”一点:
- 支持:从源目录递归遍历
- 支持:按文件名关键字搜索(包含匹配)
- 支持:把匹配到的文件备份到目标目录
- 特点:保留原目录结构(靠
relativize) - 增量策略:若目标存在且大小/修改时间相同就跳过(简单但实用)
- 可选:跳过隐藏目录、跳过超大文件(示例里留了开关)
你完全可以把它当成“小型备份命令行工具”的雏形。
6.1 完整代码:FileSearchBackupTool.java
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Objects;
import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
public class FileSearchBackupTool {
// ——你可以按需要改这些策略——
private final boolean skipHiddenDirs = true;
private final long maxFileSizeBytes = 50L * 1024 * 1024; // 50MB,示例:超大文件先不备份
private final boolean enableMaxSizeLimit = false; // 默认不限制,想限制就改 true
public static void main(String[] args) throws IOException {
if (args.length < 3) {
System.out.println("""
用法:
java FileSearchBackupTool <sourceDir> <targetDir> <keyword>
示例:
java FileSearchBackupTool /data/src /data/backup report
""");
return;
}
Path sourceDir = Path.of(args[0]);
Path targetDir = Path.of(args[1]);
String keyword = args[2];
new FileSearchBackupTool().run(sourceDir, targetDir, keyword);
}
public void run(Path sourceDir, Path targetDir, String keyword) throws IOException {
Objects.requireNonNull(sourceDir);
Objects.requireNonNull(targetDir);
Objects.requireNonNull(keyword);
if (!Files.isDirectory(sourceDir)) {
throw new IllegalArgumentException("sourceDir 不是目录: " + sourceDir);
}
Files.createDirectories(targetDir);
System.out.println("开始搜索并备份:");
System.out.println(" source = " + sourceDir.toAbsolutePath().normalize());
System.out.println(" target = " + targetDir.toAbsolutePath().normalize());
System.out.println(" keyword = " + keyword);
System.out.println();
Files.walkFileTree(sourceDir, new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (skipHiddenDirs && Files.isHidden(dir)) {
System.out.println("跳过隐藏目录: " + dir);
return FileVisitResult.SKIP_SUBTREE;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (!attrs.isRegularFile()) return FileVisitResult.CONTINUE;
if (enableMaxSizeLimit && attrs.size() > maxFileSizeBytes) {
System.out.println("跳过超大文件: " + file + " (" + attrs.size() + " bytes)");
return FileVisitResult.CONTINUE;
}
String name = file.getFileName().toString();
if (!name.contains(keyword)) return FileVisitResult.CONTINUE;
// 计算相对路径,保留目录结构
Path relative = sourceDir.relativize(file);
Path dest = targetDir.resolve(relative);
Files.createDirectories(dest.getParent());
if (shouldSkipCopy(file, dest)) {
System.out.println("已存在且无变化,跳过: " + relative);
return FileVisitResult.CONTINUE;
}
Files.copy(file, dest, REPLACE_EXISTING, COPY_ATTRIBUTES);
System.out.println("备份完成: " + relative);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
// 权限/符号链接/IO 错误等,都别让任务整体中断
System.err.println("访问失败: " + file + " -> " + exc.getMessage());
return FileVisitResult.CONTINUE;
}
});
System.out.println();
System.out.println("搞定✅(如果你看到一堆跳过/失败日志,别慌,多半是权限或隐藏目录策略在生效)");
}
/**
* 一个简单的“增量拷贝”判断:
* - 目标文件不存在:复制
* - 目标存在且大小一致、最后修改时间一致:跳过
*
* 说明:这不是严格的内容一致校验(比如 hash),但对很多备份场景足够好用。
*/
private boolean shouldSkipCopy(Path src, Path dest) {
try {
if (!Files.exists(dest)) return false;
BasicFileAttributes s = Files.readAttributes(src, BasicFileAttributes.class);
BasicFileAttributes d = Files.readAttributes(dest, BasicFileAttributes.class);
return s.size() == d.size()
&& s.lastModifiedTime().equals(d.lastModifiedTime());
} catch (IOException e) {
// 读属性失败就别跳过,宁可多复制一次,也别漏备份
return false;
}
}
}
6.2 这个例子“真正能打”的地方在哪?
别看它不大,但它已经具备一些“现实世界的脾气”:
- 不靠字符串拼路径:全程
Path.resolve / relativize - 递归遍历用 FileVisitor:可控、可跳过、可容错
- copy 带 COPY_ATTRIBUTES:尽量保留时间戳等属性(备份更像备份)
- 失败不中断:碰到权限问题不至于全盘崩
- 增量策略可换:你想升级成 hash 校验、按时间窗口备份、生成 manifest,都很自然
要是你愿意再往前走一步,我们还可以加:
--glob支持:按*.log这类模式匹配- 并发复制(线程池)
- 备份清单(记录本次备份文件、大小、耗时)
- 删除目标多余文件(同步模式,谨慎启用)
收尾:java.nio.file 的“正确打开方式”,其实就一句话
把路径当 Path,把文件操作交给 Files,把遍历交给 DirectoryStream/FileVisitor,把属性权限当成可选能力而不是默认存在。
你如果还在用 File + 手搓字符串路径写工具,我不说你不行,但我真心建议你试试 nio.file:写起来更像在“做事”,而不是在“跟系统吵架”。😄
📝 写在最后
如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!
我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!
感谢你的阅读,我们下篇文章再见~👋
✍️ 作者:某个被流“治愈”过的 Java 老兵
📅 日期:2026-01-07
🧵 本文原创,转载请注明出处。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)