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");

        // 取得所有頂層檔案與資料夾的 stream
        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)

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION