Java以副檔名尋找指定目錄下的文件
一、簡介
在本快速教學中,我們將看到一些使用核心 Java 和外部程式庫來搜尋目錄(包括子目錄)中與特定副檔名相符的檔案的替代方案。我們將從簡單的陣列和列表轉向流和其他更新的方法。
2. 設定我們的過濾器
由於我們需要按擴展名過濾文件,因此讓我們從一個簡單的Predicate
實作開始。我們需要一些輸入清理以確保匹配大多數用例,例如接受是否以點開頭的擴展名稱:
public class MatchExtensionPredicate implements Predicate<Path> {
private final String extension;
public MatchExtensionPredicate(String extension) {
if (!extension.startsWith(".")) {
extension = "." + extension;
}
this.extension = extension.toLowerCase();
}
@Override
public boolean test(Path path) {
if (path == null) {
return false;
}
return path.getFileName()
.toString()
.toLowerCase()
.endsWith(extension);
}
}
我們首先編寫建構函數,該構造函數在擴展名稱之前添加一個點(如果它尚未包含點)。然後,我們將其轉換為小寫。這樣,當我們將其與其他文件進行比較時,我們可以確保它們具有相同的大小寫。最後,我們透過取得Path
的檔案名稱並將其轉換為小寫來實作test()
。最重要的是,我們檢查它是否以我們正在尋找的擴展名結尾。
3.使用Files.listFiles()
遍歷目錄
我們的第一個範例將使用自 Java 誕生以來就存在的方法: Files.listFiles()
。讓我們先實例化一個List
來儲存我們的結果並列出目錄中的所有檔案:
List<File> find(File startPath, String extension) {
List<File> matches = new ArrayList<>();
File[] files = startPath.listFiles();
if (files == null) {
return matches;
}
// ...
}
listFiles()
本身並不會遞歸操作,因此對於每個項目,如果我們確定它是一個目錄,我們就開始遞歸:
MatchExtensionPredicate filter = new MatchExtensionPredicate(extension);
for (File file : files) {
if (file.isDirectory()) {
matches.addAll(find(file, extension));
} else if (filter.test(file.toPath())) {
matches.add(file);
}
}
return matches;
我們還實例化我們的filter
,並且僅在當前文件通過我們的test()
實現時才將其添加到我們的列表中。最終,我們將獲得與過濾器匹配的所有結果。請注意,這可能會導致太深的目錄樹中出現StackOverflowError
,以及包含太多檔案的目錄中出現OutOfMemoryError
。稍後我們會看到性能更好的選項。
4. 從 Java 7 開始使用Files.walkFileTree()
遍歷目錄
從 Java 7 開始,我們有了 NIO2 API。它包括許多實用程序,例如Files
類別和使用Path
類別處理檔案的新方法。使用walkFileTree()
允許我們以零努力遞歸地遍歷目錄。此方法只需要一個起始Path
和一個FileVisitor
實作:
List<Path> find(Path startPath, String extension) throws IOException {
List<Path> matches = new ArrayList<>();
Files.walkFileTree(startPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
if (new MatchExtensionPredicate(extension).test(file)) {
matches.add(file);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
return FileVisitResult.CONTINUE;
}
});
return matches;
}
FileVisitor
包含一些事件的回呼:進入目錄之前、離開目錄之後、存取檔案時、存取失敗時。但是,使用SimpleFileVisitor
,我們只需要實作我們感興趣的回呼。在本例中,它是使用visitFile()
存取檔案。因此,對於存取的每個文件,我們都會Predicate
對其進行測試並將其添加到匹配文件列表中。
此外,我們正在實作visitFileFailed()
以始終傳回FileVisitResult.CONTINUE
。這樣,即使發生異常(例如存取被拒絕),我們也可以繼續搜尋文件。
5. 從 Java 8 開始使用Files.walk()
進行串流傳輸
Java 8 提供了一種更簡單的方式來遍歷與Stream
API 整合的目錄。這是我們的方法使用Files.walk()
的樣子:
Stream<Path> find(Path startPath, String extension) throws IOException {
return Files.walk(startPath)
.filter(new MatchExtensionPredicate(extension));
}
不幸的是,這在第一次拋出異常時就中斷了,而且還沒有辦法處理這個問題。那麼,讓我們嘗試一種不同的方法。我們首先實作一個包含 ConsumerFileVisitor
。這次,我們將使用此Consumer
對文件匹配執行任何我們想要的操作,而不是將它們累積在List
中:
public class SimpleFileConsumerVisitor extends SimpleFileVisitor<Path> {
private final Predicate<Path> filter;
private final Consumer<Path> consumer;
public SimpleFileConsumerVisitor(MatchExtensionPredicate filter, Consumer<Path> consumer) {
this.filter = filter;
this.consumer = consumer;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
if (filter.test(file)) {
consumer.accept(file);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
}
最後,讓我們修改find()
方法來使用它:
void find(Path startPath, String extension, Consumer<Path> consumer) throws IOException {
MatchExtensionPredicate filter = new MatchExtensionPredicate(extension);
Files.walkFileTree(startPath, new SimpleFileConsumerVisitor(filter, consumer));
}
請注意,我們必須傳回Files.walkFileTree()
才能使用FileVisitor
實作。
6. 使用 Apache Commons IO 的FileUtils.iterateFiles()
另一個有用的選項是來自 Apache Commons IO 的FileUtils.iterateFiles()
,它會傳回一個Iterator
。讓我們包括它的依賴項:
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.13.0</version>
</dependency>
憑藉其依賴性,我們還可以使用WildcardFileFilter
而不是我們的MatchExtensionPredicate
:
Iterator<File> find(Path startPath, String extension) {
if (!extension.startsWith(".")) {
extension = "." + extension;
}
return FileUtils.iterateFiles(
startPath.toFile(),
WildcardFileFilter.builder().setWildcards("*" + extension).get(),
TrueFileFilter.INSTANCE);
}
我們透過確保擴充名稱採用預期格式來開始我們的方法。檢查是否有必要在前面添加一個點,如果我們傳遞“.extension”或只是“extension”,我們的方法可以工作。
與其他方法一樣,它只需要一個起始目錄。但是,由於這是一個較舊的 API,因此它需要File
而不是Path
。最後一個參數是可選的目錄過濾器。但是,如果未指定,它將忽略子目錄。因此,我們包含一個TrueFileFilter.INSTANCE
以確保存取整個目錄樹。
七、結論
在本文中,我們探索了根據指定副檔名在目錄及其子目錄中搜尋檔案的各種方法。我們先設定一個靈活的擴充匹配Predicate
。然後,我們介紹了不同的技術,從傳統的Files.listFiles()
和Files.walkFileTree()
方法到 Java 8 中引入的更現代的替代方法,例如Files.walk()
。此外,我們從不同的角度探討了 Apache Commons IO 的FileUtils.iterateFiles()
的用法。
與往常一樣,原始碼可以在 GitHub 上取得。