CodeGym /课程 /JAVA 25 SELF /批量文件操作

批量文件操作

JAVA 25 SELF
第 40 级 , 课程 2
可用

1. 遍历目录中的文件

获取文件列表:Files.list 与 Files.walk

在 Java 中有两个非常好用的方法可用于处理文件夹内容:

  • Files.list(Path) — 返回给定目录顶层(不递归)的所有文件和文件夹的流(Stream<Path>)。
  • Files.walk(Path) — 返回目录及其所有子目录(递归)中的所有文件和文件夹的流。

示例:输出目录 data 中的所有文件和文件夹

import java.nio.file.*;
import java.io.IOException;

public class ListFilesExample {
    public static void main(String[] args) throws IOException {
        Path dir = Paths.get("data");

        // 获取所有顶层文件和文件夹的流
        try (var stream = Files.list(dir)) {
            stream.forEach(System.out::println);
        }
    }
}

如果你想递归遍历所有子文件夹,请使用 Files.walk

try (var stream = Files.walk(dir)) {
    stream.forEach(System.out::println);
}

注意:
Files.walk 在结构很大的情况下可能返回非常多的文件——不要在磁盘根目录上运行,否则你会看到“整个宇宙的清单”!

可视化:list 与 walk 的区别

方法 返回内容
Files.list
仅当前文件夹的内容
Files.walk
所有文件和文件夹,包括嵌套的

2. 文件过滤

使用流(Stream<Path>)的最大优势之一是可以按任意条件过滤文件:扩展名、名称、大小、创建日期等。

按扩展名过滤

假设我们只需要 .txt 文件,可以这样过滤:

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".txt"))
        .forEach(System.out::println);
}

按名称过滤

获取文件名以 "report" 开头的所有文件:

stream
    .filter(path -> path.getFileName().toString().startsWith("report"))
    .forEach(System.out::println);

按大小过滤

只获取“较大”的文件(大于 1 MB):

stream
    .filter(path -> {
        try {
            return Files.size(path) > 1_000_000;
        } catch (IOException e) {
            return false;
        }
    })
    .forEach(System.out::println);

按日期过滤

例如,最近 7 天内修改的文件:

import java.time.*;
import java.nio.file.attribute.*;

stream
    .filter(path -> {
        try {
            FileTime lastModified = Files.getLastModifiedTime(path);
            return lastModified.toInstant().isAfter(Instant.now().minus(Duration.ofDays(7)));
        } catch (IOException e) {
            return false;
        }
    })
    .forEach(System.out::println);

组合过滤:多条件一起

可以像“点菜”一样组合过滤条件:例如,筛选一个月之前且大于 10 KB 的所有 .log 文件:

stream
    .filter(path -> path.toString().endsWith(".log"))
    .filter(path -> {
        try {
            return Files.size(path) > 10_000;
        } catch (IOException e) {
            return false;
        }
    })
    .filter(path -> {
        try {
            FileTime lastModified = Files.getLastModifiedTime(path);
            return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(30)));
        } catch (IOException e) {
            return false;
        }
    })
    .forEach(System.out::println);

3. 批量复制与删除文件

批量操作很简单:先获取文件流,然后对每个元素应用所需函数。关键是不要忽略可能的错误(例如,文件被占用或目标已存在)。

将一个文件夹中的所有文件复制到另一个文件夹

import java.nio.file.StandardCopyOption;

Path sourceDir = Paths.get("data");
Path targetDir = Paths.get("backup");

try (var stream = Files.list(sourceDir)) {
    stream.forEach(path -> {
        Path targetPath = targetDir.resolve(path.getFileName());
        try {
            Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
            System.out.println("已复制:" + path.getFileName());
        } catch (IOException e) {
            System.out.println("复制出错 " + path.getFileName() + ":" + e.getMessage());
        }
    });
}

删除所有临时文件(*.tmp

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".tmp"))
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("已删除:" + path.getFileName());
            } catch (IOException e) {
                System.out.println("删除出错 " + path.getFileName() + ":" + e.getMessage());
            }
        });
}

使用 Stream API 处理路径集合

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".txt"))
        .sorted((p1, p2) -> {
            try {
                return Long.compare(Files.size(p2), Files.size(p1)); // 按大小,降序
            } catch (IOException e) {
                return 0;
            }
        })
        .limit(5) // 仅取最大的 5 个
        .forEach(System.out::println);
}

4. 实践任务

查找并删除早于指定日期的文件

删除一年内未修改的所有文件:

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> {
            try {
                FileTime lastModified = Files.getLastModifiedTime(path);
                return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(365)));
            } catch (IOException e) {
                return false;
            }
        })
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("已删除:" + path.getFileName());
            } catch (IOException e) {
                System.out.println("删除出错 " + path.getFileName() + ":" + e.getMessage());
            }
        });
}

按模板批量重命名文件

假设需要为每个 .txt 文件添加前缀 "old_"

try (var stream = Files.list(dir)) {
    stream
        .filter(path -> path.toString().endsWith(".txt"))
        .forEach(path -> {
            Path newPath = path.resolveSibling("old_" + path.getFileName());
            try {
                Files.move(path, newPath);
                System.out.println("已重命名:" + path.getFileName() + " -> " + newPath.getFileName());
            } catch (IOException e) {
                System.out.println("重命名出错 " + path.getFileName() + ":" + e.getMessage());
            }
        });
}

复制所有子文件夹中的所有文件(递归)

使用 Files.walk 遍历所有子文件夹中的文件:

try (var stream = Files.walk(sourceDir)) {
    stream
        .filter(Files::isRegularFile)
        .forEach(path -> {
            Path relative = sourceDir.relativize(path);
            Path targetPath = targetDir.resolve(relative);
            try {
                Files.createDirectories(targetPath.getParent());
                Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
                System.out.println("已复制:" + path + " -> " + targetPath);
            } catch (IOException e) {
                System.out.println("复制出错 " + path + ":" + e.getMessage());
            }
        });
}

5. 重要注意事项与细节

批量操作中的错误处理

在批量操作中几乎总会遇到“问题”文件:无权限、文件被占用、路径过长、目标已存在等。如果不在循环内部处理异常,程序可能会在第一个失败处“崩溃”。因此请务必在 forEach 内使用 try-catch,对单个文件的错误进行“吞掉/记录”,但继续处理其他文件。

递归对性能与调用栈的影响

对大型目录使用 Files.walk 时,如果手动编写递归,遍历路径可能会非常长。好在该方法本身实现高效,不会导致栈溢出。但如果你在编写自己的递归函数来删除/复制目录,请小心:对于巨大且深层嵌套的结构,调用栈可能会“爆掉”。

处理大型文件夹

  • 谨慎使用 Files.walk:如果文件夹中有成千上万个文件,这会耗时且占用大量内存。
  • 如果只需处理顶层内容——请使用 Files.list
  • 按名称匹配查找文件可使用(endsWithmatches 等)进行过滤。

示例:“清理文件夹中的临时文件”

Path tempDir = Paths.get("data/tmp");

try (var stream = Files.walk(tempDir)) {
    stream
        .filter(Files::isRegularFile)
        .filter(path -> path.toString().endsWith(".tmp"))
        .forEach(path -> {
            try {
                Files.delete(path);
                System.out.println("已删除:" + path);
            } catch (IOException e) {
                System.out.println("删除出错 " + path + ":" + e.getMessage());
            }
        });
}

批量操作的主要方法

操作 方法/思路 要点
获取所有文件
Files.list, Files.walk
walk—递归,list—仅顶层
按扩展名过滤
filter(path -> ...)
可与其他过滤条件组合
复制一组文件
forEach + Files.copy
请确认目标文件夹已存在
删除一组文件
forEach + Files.delete
最好为每次操作使用 try-catch
批量重命名
forEach + Files.move
重命名本质上是带新名称的 move

6. 批量操作中的常见错误

错误 1:在 forEach 内未处理的异常。
这很常见:如果不把每个操作包在 try-catch 中,程序可能会在第一个问题文件上“崩溃”,其余的将不会被处理。

错误 2:把文件夹当作文件来删除/复制(或反之)。
方法 Files.deleteFiles.copy 对文件与文件夹的处理不同。不要混淆!例如,尝试用标准方法删除非空文件夹会导致错误。

错误 3:复制时错误地构造目标路径。
如果构造目标路径时不考虑目录结构(例如未使用 relativize),可能会覆盖文件,或得到错误的归档结构。

错误 4:同时打开过多的文件。
如果在循环中打开读/写流,请不要忘记关闭!最好使用 try-with-resources

错误 5:递归遍历未限制深度。
对非常大且很深的文件夹使用 Files.walk 可能引发性能和内存问题。如果需要限制深度——请使用带深度参数的重载:Files.walk(dir, depth)

错误 6:无意中忽略隐藏/系统文件。
如果应用处理用户文件,可能会不小心涉及隐藏或系统文件。要过滤它们,请使用方法 Files.isHidden(path)

评论 (1)
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION
ncksllpo 级别 44,Cherkasy,Ukraine
4 四月 2026
😅😅😅😅