👋 你好,欢迎来到我的博客!我是【菜鸟不学编程】
   我是一个正在奋斗中的职场码农,步入职场多年,正在从“小码农”慢慢成长为有深度、有思考的技术人。在这条不断进阶的路上,我决定记录下自己的学习与成长过程,也希望通过博客结识更多志同道合的朋友。
  
  🛠️ 主要方向包括 Java 基础、Spring 全家桶、数据库优化、项目实战等,也会分享一些踩坑经历与面试复盘,希望能为还在迷茫中的你提供一些参考。
  💡 我相信:写作是一种思考的过程,分享是一种进步的方式。
  
   如果你和我一样热爱技术、热爱成长,欢迎关注我,一起交流进步!

前言 😮‍💨

我先承认:我当年也用 java.io.File 写过一堆“看起来能跑、但越看越心虚”的代码。什么 new File("a/b")listFiles() 返回 null 还得防、路径拼接靠手写斜杠……写到最后像在跟操作系统猜谜。
  后来我换到 java.nio.file 才发现:不是我变强了,是工具终于像个人了。😂
  这一篇我按你给的大纲来,从 PathFiles,从目录流遍历到文件树递归,再到权限属性,最后给一个能落地的完整例子:文件搜索 + 增量备份工具(含过滤规则、跳过隐藏目录、保留目录结构、可选删除目标多余文件)。全程用人话讲清楚,也给你能直接抄走的代码。

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:deleteIfExistsexists + delete 更稳

Files.deleteIfExists(Path.of("old.log"));

这比你先 Files.exists() 再删更靠谱,因为你判断存在到删除之间可能被别人动过(典型的 TOCTOU 问题)。别小看这类“间隙”,线上就爱在这种地方搞事情。😑

2.4 readAllBytes:小文件一把梭,大文件别冲动

byte[] data = Files.readAllBytes(Path.of("small.bin"));

注意:这适合小文件。大文件请用 Files.newInputStreamFiles.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
🧵 本文原创,转载请注明出处。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐