使用 Java 取得目錄及其子目錄中的檔案數量
1.概述
計算目錄及其子目錄中的檔案數量是編程中的常見任務,無論您是建立備份實用程式、監視磁碟使用情況還是跨系統同步檔案。對於 Java 開發人員來說,這個看似簡單的問題提供了探索傳統和現代文件處理方法的機會。
在本教程中,我們將深入探討兩種主要方法:許多人熟悉的遞歸java.io.File
方法,以及 Java 7 中引入的更高效的基於NIO
的解決方案。
2. 設定
統計目錄及其子目錄中的檔案數量的任務需要遍歷可能很複雜的樹狀結構,其中每個子目錄可能包含更多檔案或額外的子目錄。這種遞歸性質帶來了技術挑戰:我們如何確保各個層級的準確性,避免符號連結的無限循環,以及如何管理大型資料集的效能?
對於 Java 開發人員來說,解決方案在於選擇正確的工具並設計清晰靈活的方法。
讓我們為即將到來的實作定義方法簽署:
public long numberOfFilesIn(String path) {
// TODO: Implementation
}
此方法接受String path
輸入參數作為起始目錄,並以long
形式傳回找到的檔案數。 long
類型確保我們可以處理超出 32 位元int
限制的計數,這對於企業級目錄至關重要。
3. 使用java.io.File
我們的第一個實作是用 Java 讀取檔案並對其進行計數,將使用java.io.File
庫。
3.1.使用 Java 8+
讓我們從使用 Java 8 及更高版本的實作開始,這樣我們就可以使用Streams
以函數式的方式編寫。
我們的任務是瀏覽目錄、搜尋檔案並繼續進入更深的嵌套目錄,這是重複的。這裡有一種明顯的實現風格:遞歸。與迭代方法相比,自我調用的可能性有助於我們降低複雜性:
File currentFile = new File(path);
File[] filesOrNull = currentFile.listFiles();
// Is this a file already?
long currentFileNumber = currentFile.isFile() ? 1 : 0;
if (filesOrNull == null) { // no sub directories found
return currentFileNumber; // stop condition #1
}
return currentFileNumber + Arrays.stream(filesOrNull)
.mapToLong(FindFolder::filesInside) // <-- recursion call here
.sum();
我們的實施基本上分為三個部分:
- 檢查目前路徑是否解析為檔案或目錄
- 如果當前目錄無法列出任何文件,我們將立即返回
- 否則,我們遞歸計算子資料夾中的檔案數並傳回總數,包括當前檔案數
private static long filesInside(File it) {
if (it.isFile()) {
return 1; // stop condition #2
} else if (it.isDirectory()) {
return numberOfFilesIn(it.getAbsolutePath()); // <-- recursion to caller
} else {
return 0; // stop condition #3
}
}
請注意,我們在遞歸實作中包含了三個停止條件。如果沒有足夠注意跳出遞歸循環可能會導致記憶體不足異常。
3.2. Java 8 之前
如果我們使用的是低於版本 8 的 Java,我們可以簡單地從上面的遞歸stream
實作重構我們的方法。
這是我們的第二種方法,重寫後沒有streams
部分如下所示:
for (File file : filesOrNull) {
if (file.isDirectory()) {
currentFileNumber += numberOfFilesIn(file.getAbsolutePath());
} else if (file.isFile()) {
currentFileNumber += 1; // add this file to count
}
}
return currentFileNumber;
它看起來與我們之前使用的映射器函數非常相似,只是這次我們透過新增中間結果來修改currentFileNumber
變數。如果我們忽略可變性問題,這個解決方案看起來簡單、程式碼少、可讀性好。
4.使用NIO
在 Java 中使用檔案系統有另一個很好的替代函式庫,自 Java 7 開始提供:NIO。
4.1. Files.find
我們完成該任務的第三種方法 —— 計算目錄和子目錄中的檔案數量 —— 將利用[Files.find](https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#find-java.nio.file.Path-int-java.util.function.BiPredicate-java.nio.file.FileVisitOption...-)
功能:
try (Stream<Path> stream = Files.find(
Paths.get(path),
Integer.MAX_VALUE,
(__, attr) -> attr.isRegularFile())) {
return stream.count();
} catch (IOException e) {
// or log here
throw new RuntimeException(e);
}
我們使用資源with
在我們的初始path
目錄中開啟Path Stream
。
4.2.演練
NIO 的第二種可能性是以迭代的方式「遍歷」檔案系統。
[Files.walk](https://docs.oracle.com/javase/8/docs/api/java/nio/file/Files.html#walk-java.nio.file.Path-java.nio.file.FileVisitOption...-)
的實現如下:
Path dir = Path.of(path);
try (Stream<Path> stream = Files.walk(dir)) {
return stream.parallel()
.map(getFileOrEmpty())
.flatMap(Optional::stream)
.filter(it -> !it.isDirectory())
.count();
} catch (IOException e) {
throw new RuntimeException(e);
}
就像以前一樣,我們在初始path
目錄中獲得一個Path
Stream
。由於我們搜尋目錄的順序並不重要,並且要覆蓋的路徑數量可能很大,因此我們將其轉換為並行 S tream
。然後,我們過濾掉無法與預設提供者關聯的Paths
,並將它們包裝在 Java Optional
中。最後,傳回所有目前非目錄(即檔案)元素的數量。
private static Function<Path, Optional<File>> getFileOrEmpty() {
return it -> {
try {
return Optional.of(it.toFile());
} catch (UnsupportedOperationException e) {
// You may print or log the exception here;
return Optional.empty();
}
};
}
提取的方法getFileOrEmpty
傳回一個映射函數,以將有效的File
安全地包裝在Optional
中。它有兩個用途:保持呼叫者方法較小,並處理UnsupportedOperationException
,後者不應在 S tream
中未處理。
5.總體考慮
實施文件計數解決方案時有幾個因素值得注意。效能至關重要——由於堆疊溢位風險,遞歸java.io.File
方法可能難以處理深層目錄,而 NIO 的基於流的方法可以更好地適應大型資料集。對於廣泛的結構,請考慮使用Files.walk
的平行流,但這需要小心處理並發檔案修改。
安全性也很重要:如果使用者輸入驅動路徑,請驗證它以防止目錄遍歷攻擊。
此外,利用 NIO 的 Unicode 支援確保與國際檔案名稱的兼容性,避免非 ASCII 字元的問題。
平衡效率、安全性和穩健性可確保這些方法有效滿足現實世界的需求。
6.驗證
現在我們已經了解了所有不同的實現,讓我們設計一個測試來運行。
由於驗證必須通過真實目錄,因此讓我們建立一些文件和資料夾來搜尋:
filesToBeFound
|-- file1.txt
|-- subEmptyFolder
|-- subFolder1
|-- file2.txt
|-- file3.txt
|-- subFolder2
|-- file4.txt
|-- subSubFolder
|-- subSubSubFolder
|-- file5.txt
唯一剩下的部分就是測試本身:
private final String resourcePath = this.getClass().getResource("/filesToBeFound").getPath();
@Test
void shouldReturnNumberOfAllFilesInsidePath() {
assertThat(FindFolder.numberOfFilesIn(resourcePath)).isEqualTo(5);
}
我們的每個實作都應該在安裝目錄中找到相同的五個檔案。
7. 結論
在本教程中,我們探討了兩種使用 Java 統計目錄及其子目錄中檔案數量的強大方法。 java.io.File
方法具有遞歸的簡單性,適合較小、較淺的目錄結構,並為開發人員提供了清晰的切入點。然而,它對遞歸的依賴可能會因深層而失效,從而有導致堆疊溢位錯誤的風險。相較之下,基於 NIO 的解決方案(使用Files.find
和Files.walk
)提供了效率和可擴展性,利用串流來處理大型資料集。並行Files.walk
選項進一步優化了廣泛目錄的性能,儘管它需要仔細的異常處理。
對於大多數現代應用程式來說,NIO 方法因其穩健性和效能而成為更好的選擇。對於快速、簡單的任務,請選擇java.io.File
,但當可擴充性很重要時,請選擇 NIO。
與往常一樣,本文使用的完整程式碼可以在 GitHub 上找到。