使用flatMap列出子目錄
前面已經看到如何列出指定目錄下的檔案了。我們再來看下如何遍歷指定目錄的直接子目錄(深度為1),先實作一個簡單的版本,然後再用更方便的flatMap()方法來實作。
我們先用傳統的for迴圈來遍歷一個指定的目錄。如果子目錄中有文件,就加到列表裡;否則就把子目錄加到列表裡。最後,列印出所有文件的總數。程式碼在下面――這個是困難模式的。
複製代碼代碼如下:
public static void listTheHardWay() {
List<File> files = new ArrayList<>();
File[] filesInCurrentDir = new File(".").listFiles();
for(File file : filesInCurrentDir) {
File[] filesInSubDir = file.listFiles();
if(filesInSubDir != null) {
files.addAll(Arrays.asList(filesInSubDir));
} else {
files.add(file);
}
}
System.out.println("Count: " + files.size())
}
我們先取得目前目錄下的文件列表,然後進行遍歷。對於每個文件,如果它有子文件,就把它們加到清單中。這樣做是沒問題的,不過它有一些常見的問題:可變性,基本類型偏執,命令式,程式碼冗長,等等。一個叫flatMap()的小方法就可以解決掉這些問題。
正如這個名字所說的,這個方法在映射後會進行扁平化。它會像map()一樣對集合中的元素進行映射。但是和map()方法不同的是,map()方法裡面的lambda表達式只是回傳一個元素,而這裡回傳的是一個Stream物件。於是這個方法將多個流壓平,將裡面的每個元素映射到一個扁平化的流。
我們可以用flatMap()來執行各種操作,不過現在手邊的這個問題就剛好詮釋了它的價值。每個子目錄都有一個檔案的清單或說流,而我們希望取得目前目錄下的所有子目錄中的檔案清單。
有一些目錄可能是空的,或者說沒有子元素。在這種情況下,我們將這個空目錄或檔案包裝成一個流物件。如果我們想要忽略某個文件,JDK中的flatMap()方法也可以很好的處理空文件;它會把一個空引用作為一個空集合合併到流裡。來看下flatMap()方法的使用。
複製代碼代碼如下:
public static void betterWay() {
List<File> files =
Stream.of(new File(".").listFiles())
.flatMap(file -> file.listFiles() == null ?
Stream.of(file) : Stream.of(file.listFiles()))
.collect(toList());
System.out.println("Count: " + files.size());
}
我們先是取得了目前目錄的子檔案流,然後呼叫了它的flatMap()方法。然後將一個lambda表達式傳給這個方法,這個表達式會傳回指定檔案的子檔案的流。 flatMap()方法傳回的是目前目錄所有子目錄下的檔案的集合。我們使用collect()方法以及Collectors裡面的toList()(方法把它們收集到一個清單中。
我們傳給flatMap()的這個lambda表達式,它回傳的是一個檔案的子檔。 如果沒有的話,則傳回這個檔案的流。 flatMap()方法優雅地將這個流映射到一個流的集合中,然後將這個集合扁平化,最終合併到一個流中。
flatMap()方法減少了許多開發的工作――它將兩個連續的操作很好的結合到了一起,這通常稱為元組――用一個優雅的操作就完成了。
我們已經知道如何使用flatMap()方法將直接子目錄中的所有檔案列出來。下面我們來監控一下文件的修改操作。
監控文件修改
我們已經知道如何找到文件及目錄,不過如果我們希望在文件創建,修改或刪除的時候,能夠接收到提示訊息的話,這個也非常簡單。這樣的機制對於監視一些特殊文件例如設定文件,系統資源的改變非常有用。下面我們來探索下Java 7中引入的這個工具,WatchService,它可以用來監控檔案的修改。下面我們看到的許多特性都來自JDK 7,而這裡最大的改進就是內部迭代器帶來的便利性。
我們先來寫個監控目前目錄中的檔案修改的例子。 JDK中的Path類別會對應檔案系統中的一個實例,它是一個觀察者服務的工廠。我們可以給這個服務註冊通知事件,就像這樣:
複製代碼代碼如下:
inal Path path = Paths.get(".");
final WatchService watchService =
path.getFileSystem()
.newWatchService();
path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
System.out.println("Report any file changed within next 1 minute...");
我們註冊了一個WatchService來觀察目前目錄的修改。你可以輪詢這個WatchService來取得目錄下檔案的修改操作,它會透過一個WatchKey將這些改動回給我們。一旦我們拿到了這個key,可以遍歷它的所有事件來獲取文件更新的詳細資訊。因為可能會有多個檔案同時修改,poll操作可能會傳回多個事件。來看下輪詢以及遍歷的程式碼。
複製代碼代碼如下:
final WatchKey watchKey = watchService.poll(1, TimeUnit.MINUTES);
if(watchKey != null) {
watchKey.pollEvents()
.stream()
.forEach(event ->
System.out.println(event.context()));
}
這裡可以看到,Java 7和Java 8的特性同時出場了。我們把pollEvents()傳回的集合轉換成了一個Java 8的Stream,然後使用它的內部迭代器來列印出每個檔案的詳細的更新資訊。
讓我們來執行下這段程式碼,然後將目前目錄下的sample.txt檔案修改一下,看看這個程式是否能察覺這個更新。
複製代碼代碼如下:
Report any file changed within next 1 minute...
sample.txt
當我們修改了這個檔案的時候,程式會提示說檔案被修改了。我們可以用這個功能來監視不同檔案的更新,然後執行對應的任務。當然我們也可以只註冊檔案新建或刪除的操作。
總結
有了lambda表達式和方法引用後,像是字串及檔案的操作,創建自訂比較器這些常見的任務都變得更簡單也更簡潔了。匿名內部類也變得優雅起來了,而可變性就像日出後的晨霧一樣,也消失得無影無蹤了。使用這種新風格進行編碼還有一個福利,就是你可以使用JDK的新設施來有效地遍歷龐大的目錄。
現在你已經知道如何建立lambda表達式並把它傳遞給方法了。下一章我們將介紹如何使用函數式介面及lambda表達式進行軟體的設計。