首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java 文件複製與移動實戰:Apache Commons IO、NIO.2 與遞歸遍歷

     編輯:Java 程式設計入門

文件複製/移動是基礎操作,但大目錄、符號鏈接、權限保留、原子性、進度回調等細節決定生產可用性。本文對比三種主流方案,提供生產級工具類。

方案對比速查

方案API優點缺點適用場景
Apache Commons IOFileUtils.copyFile(), copyDirectory()一行代碼、自動處理目錄遞歸、保留時間戳依賴第三方、大文件無進度、無原子性保證中小文件、快速開發
Java NIO.2 (Java 7+)Files.copy(), move(), walkFileTree()零依賴、支援原子移動、屬性複製、流式處理API 較繁瑣、需自行實現遞歸/過濾生產環境、大文件、跨文件系統
手工流 (InputStream/OutputStream)自定義緩衝區完全控制、可插入加密/壓縮/進度易出錯、重複造輪子特殊需求 (加密複製、斷點續傳)

NIO.2 核心 API 詳解

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.FileTime;
import java.util.EnumSet;

public class FileCopyMoveUtil {

    /** 複製單文件:支援覆蓋、屬性複製、原子性 (同文件系統) */
    public static void copyFile(Path src, Path dest, boolean overwrite) throws IOException {
        CopyOption[] options = overwrite ? 
            new CopyOption[]{StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES} :
            new CopyOption[]{StandardCopyOption.COPY_ATTRIBUTES};
        Files.copy(src, dest, options);
    }

    /** 移動文件:同文件系統原子,跨文件系統複製+刪除 */
    public static void moveFile(Path src, Path dest, boolean overwrite) throws IOException {
        CopyOption[] options = overwrite ?
            new CopyOption[]{StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE} :
            new CopyOption[]{StandardCopyOption.ATOMIC_MOVE};
        try {
            Files.move(src, dest, options);
        } catch (AtomicMoveNotSupportedException e) {
            // 跨文件系統退回複製+刪除
            copyFile(src, dest, overwrite);
            Files.deleteIfExists(src);
        }
    }

    /** 遞歸複製目錄:支援過濾、進度回調 */
    public static void copyDirectory(Path srcDir, Path destDir, 
                                     DirectoryStream.Filter filter,
                                     CopyProgressCallback callback) throws IOException {
        Files.walkFileTree(srcDir, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
            new SimpleFileVisitor() {
                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                    Path targetDir = destDir.resolve(srcDir.relativize(dir));
                    if (!Files.exists(targetDir)) Files.createDirectories(targetDir);
                    if (callback != null) callback.onDirectory(dir, targetDir);
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    if (filter == null || filter.accept(file)) {
                        Path targetFile = destDir.resolve(srcDir.relativize(file));
                        copyFile(file, targetFile, true);
                        if (callback != null) callback.onFile(file, targetFile, attrs.size());
                    }
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFileFailed(Path file, IOException exc) {
                    if (callback != null) callback.onError(file, exc);
                    return FileVisitResult.CONTINUE; // 或 TERMINATE
                }
            });
    }

    @FunctionalInterface
    public interface CopyProgressCallback {
        void onDirectory(Path srcDir, Path destDir);
        void onFile(Path srcFile, Path destFile, long size);
        void onError(Path file, IOException e);
    }
}

Apache Commons IO 簡化寫法

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.15.1</version>
</dependency>

// 複製文件
FileUtils.copyFile(srcFile, destFile); // 自動建立父目錄
FileUtils.copyFile(srcFile, destFile, true); // 保留時間戳

// 複製目錄 (遞歸)
FileUtils.copyDirectory(srcDir, destDir);
FileUtils.copyDirectory(srcDir, destDir, file -> file.getName().endsWith(".jar")); // 過濾

// 移動 (實為複製+刪除)
FileUtils.moveFile(srcFile, destFile);
FileUtils.moveDirectory(srcDir, destDir);

大文件複製進度監控 (NIO 通道 + 緩衝區)

public static void copyWithProgress(Path src, Path dest, ProgressListener listener) throws IOException {
    long total = Files.size(src);
    long copied = 0;
    try (FileChannel in = FileChannel.open(src, StandardOpenOption.READ);
         FileChannel out = FileChannel.open(dest, StandardOpenOption.CREATE, StandardOpenOption.WRITE,
                 StandardOpenOption.TRUNCATE_EXISTING)) {
        
        long position = 0;
        final int BUFFER_SIZE = 1024 * 1024; // 1MB 直接緩衝區
        while (position < total) {
            long transferred = in.transferTo(position, BUFFER_SIZE, out);
            if (transferred == 0) break; // EOF
            position += transferred;
            copied += transferred;
            if (listener != null) listener.onProgress(copied, total);
        }
    }
    if (listener != null) listener.onComplete();
}

@FunctionalInterface
public interface ProgressListener {
    void onProgress(long copied, long total);
    default void onComplete() {}
}

符號鏈接與權限處理

  • Files.copy(src, dest, LinkOption.NOFOLLOW_LINKS):複製鏈接本身而非目標。
  • StandardCopyOption.COPY_ATTRIBUTES:複製權限、時間戳、擁有者 (需權限)。
  • Windows 符號鏈接需管理員權限或開發者模式。

單元測試 (JUnit 5 + @TempDir)

@Test
void testCopyDirectoryWithFilter(@TempDir Path tempDir) throws IOException {
    Path src = tempDir.resolve("src");
    Path dest = tempDir.resolve("dest");
    Files.createDirectories(src.resolve("sub"));
    Files.writeString(src.resolve("a.txt"), "A");
    Files.writeString(src.resolve("sub/b.jar"), "B");
    Files.writeString(src.resolve("sub/c.log"), "C");

    FileCopyMoveUtil.copyDirectory(src, dest, 
        p -> p.toString().endsWith(".jar"), // 只複製 jar
        null);
    
    assertTrue(Files.exists(dest.resolve("sub/b.jar")));
    assertFalse(Files.exists(dest.resolve("a.txt")));
    assertFalse(Files.exists(dest.resolve("sub/c.log")));
}

最佳實踐清單

  1. 優先 Files.move(ATOMIC_MOVE) 實現同盤原子重命名。
  2. 大文件 (>100MB) 使用 FileChannel.transferTo() 零拷貝 (OS 支援時)。
  3. 跨文件系統移動 = 複製 + 校驗 + 刪除源,勿直接 Files.move 依賴 fallback。
  4. 目錄遍歷用 walkFileTree 而非 listFiles(),避免深度遞歸棧溢出。
  5. 生產環境必加:校驗和 (CRC32/MD5)、進度回調、錯誤重試、磁碟空間預檢。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved