384. Java IO API - Java 文件复制工具:Copy 示例完整解析
这是一个用 Java 编写的递归文件复制工具,模仿了文件系统复制的操作。它从源目录复制文件到目标目录,并支持指定复制的最大目录层级(-depth)。
java
The Copy Example
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.EnumSet;
import java.util.stream.Stream;
import static java.nio.file.FileVisitResult.CONTINUE;
import static java.nio.file.FileVisitResult.SKIP_SUBTREE;
import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
/**
* Sample code that copies files recursively
* from a source directory to a destination folder.
* The maximum number of directory levels to copy
* is specified after -depth.
* The number of files copied is printed
* to standard out.
* You can execute the application using:
* @code java Copy . new -depth 4
*/
public class Copy {
/**
* A {@code FileVisitor} that finds
* all files that match the
* specified pattern.
*/
public static class Replicator
extends SimpleFileVisitor<Path> {
Path source;
Path destination;
public Replicator(Path source, Path destination) {
this.source = source;
this.destination = destination;
}
// Prints the total number of
// files copied to standard out.
void done() throws IOException {
try (Stream<Path> path = Files.list(Paths.get(destination.toUri()))) {
System.out.println("Number of files copied: "
+ path.filter(p -> p.toFile().isFile()).count());
}
}
// Copy a file in destination
@Override
public FileVisitResult visitFile(Path file,
BasicFileAttributes attrs) {
System.out.println("Copy file: " + file);
Path newFile = destination.resolve(source.relativize(file));
try{
Files.copy(file,newFile);
}
catch (IOException ioException){
//log it and move
}
return CONTINUE;
}
// Invoke copy of a directory.
@Override
public FileVisitResult preVisitDirectory(Path dir,
BasicFileAttributes attrs) {
System.out.println("Copy directory: " + dir);
Path targetDir = destination.resolve(source.relativize(dir));
try {
Files.copy(dir, targetDir, REPLACE_EXISTING, COPY_ATTRIBUTES);
} catch (IOException e) {
System.err.println("Unable to create " + targetDir + " [" + e + "]");
return SKIP_SUBTREE;
}
return CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (exc == null) {
Path destination = this.destination.resolve(source.relativize(dir));
try {
FileTime time = Files.getLastModifiedTime(dir);
Files.setLastModifiedTime(destination, time);
} catch (IOException e) {
System.err.println("Unable to copy all attributes to: " + destination + " [" + e + "]");
}
} else {
throw exc;
}
return CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file,
IOException exc) {
if (exc instanceof FileSystemLoopException) {
System.err.println("cycle detected: " + file);
} else {
System.err.format("Unable to copy:" + " %s: %s%n",
file, exc);
}
return CONTINUE;
}
}
static void usage() {
System.err.println("java Copy <source> <destination>" +
" -depth \"<max_level_dir>\"");
System.exit(-1);
}
public static void main(String[] args)
throws IOException {
if (args.length < 4 || !args[2].equals("-depth"))
usage();
Path source = Paths.get(args[0]);
Path destination = Paths.get(args[1]);
int depth = Integer.parseInt(args[3]);
Replicator walk = new Replicator(source, destination);
EnumSet<FileVisitOption> opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
Files.walkFileTree(source, opts, depth, walk);
walk.done();
}
}
📝 功能简介
该程序会:
- 递归地将源目录中的文件复制到目标目录。
- 允许通过
-depth参数指定最大目录层级。 - 在程序结束时,打印成功复制的文件数。
例如:
java
$ java Copy . new -depth 4
会把当前目录及其子目录(最多 4 层)中的文件复制到 new 目录。
✅ 示例代码结构详解
1️⃣ main() 方法:程序入口
java
public static void main(String[] args) throws IOException {
if (args.length < 4 || !args[2].equals("-depth"))
usage();
Path source = Paths.get(args[0]);
Path destination = Paths.get(args[1]);
int depth = Integer.parseInt(args[3]);
Replicator walk = new Replicator(source, destination);
EnumSet<FileVisitOption> opts = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
Files.walkFileTree(source, opts, depth, walk);
walk.done();
}
- 输入参数解析 :首先检查输入是否符合规范,若不符合则调用
usage()打印使用说明。 - 路径和深度解析:从命令行参数中提取源目录、目标目录和最大目录深度。
- 文件遍历 :使用
Files.walkFileTree()递归遍历源目录,进行复制操作。
2️⃣ Replicator 类:文件复制核心逻辑
java
public static class Replicator extends SimpleFileVisitor<Path> {
该类继承 SimpleFileVisitor<Path> 并重写了几个关键方法来执行复制操作。
🧩 构造方法:初始化源路径与目标路径
java
public Replicator(Path source, Path destination) {
this.source = source;
this.destination = destination;
}
source:源目录路径;destination:目标目录路径。
🔍 visitFile() 方法:复制文件
java
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("Copy file: " + file);
Path newFile = destination.resolve(source.relativize(file));
try {
Files.copy(file, newFile);
} catch (IOException ioException) {
// 错误日志处理
}
return CONTINUE;
}
- 使用
Files.copy()复制文件到目标路径。 source.relativize(file)计算文件的相对路径,以保持源文件夹结构。- 如果复制失败,会捕获并记录异常,但继续执行。
📂 preVisitDirectory() 方法:复制目录
java
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("Copy directory: " + dir);
Path targetDir = destination.resolve(source.relativize(dir));
try {
Files.copy(dir, targetDir, REPLACE_EXISTING, COPY_ATTRIBUTES);
} catch (IOException e) {
System.err.println("Unable to create " + targetDir + " [" + e + "]");
return SKIP_SUBTREE;
}
return CONTINUE;
}
Files.copy()会复制目录及其属性。- 如果目录已存在并且指定了
REPLACE_EXISTING,它将覆盖该目录。 - 如果复制失败,打印错误信息并跳过该子目录(使用
SKIP_SUBTREE)。
🕓 postVisitDirectory() 方法:复制目录属性
java
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (exc == null) {
Path destination = this.destination.resolve(source.relativize(dir));
try {
FileTime time = Files.getLastModifiedTime(dir);
Files.setLastModifiedTime(destination, time);
} catch (IOException e) {
System.err.println("Unable to copy all attributes to: " + destination + " [" + e + "]");
}
} else {
throw exc;
}
return CONTINUE;
}
- 在目录复制后,复制目录的最后修改时间等属性。
- 如果复制失败,打印错误信息。
⚠️ visitFileFailed() 方法:错误处理
java
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
if (exc instanceof FileSystemLoopException) {
System.err.println("Cycle detected: " + file);
} else {
System.err.format("Unable to copy: %s: %s%n", file, exc);
}
return CONTINUE;
}
- 处理访问失败的文件。
- 如果检测到文件系统循环(符号链接等),打印循环错误信息。
3️⃣ done() 方法:统计已复制文件数
java
void done() throws IOException {
try (Stream<Path> path = Files.list(Paths.get(destination.toUri()))) {
System.out.println("Number of files copied: "
+ path.filter(p -> p.toFile().isFile()).count());
}
}
- 在文件复制完成后,统计并打印成功复制的文件数。
4️⃣ usage() 方法:程序使用说明
java
static void usage() {
System.err.println("java Copy <source> <destination> -depth \"<max_level_dir>\"");
System.exit(-1);
}
- 如果输入参数无效,打印使用说明并退出程序。
🧠 扩展思路与练习建议
| 扩展功能 | 实现方法 |
|---|---|
| 复制文件时跳过某些类型 | 在 visitFile() 方法中加入文件类型判断(如跳过 .txt 文件) |
| 并行复制大文件夹 | 使用 ExecutorService 执行并行文件复制 |
| 复制过程中显示进度条 | 使用 System.out.print 打印进度信息 |
| 按文件类型筛选复制 | 在 visitFile() 中加入文件类型过滤,例如只复制 .java 文件 |
| 备份和版本控制 | 在 visitFile() 中实现版本号控制,避免覆盖旧版本 |
🔔 错误处理建议
- 为
IOException提供更加详细的日志,例如记录文件复制失败的具体原因,或者保存失败文件列表。 - 为
visitFileFailed()方法添加更多的错误类型处理,如权限不足等。
✅ 结语
这个 Copy 示例演示了如何使用 Java 的 nio 包进行递归文件复制。通过自定义 FileVisitor 类,你可以实现高效且灵活的文件操作。这个工具可以被扩展和定制化,满足各种实际项目中的需求。