List subdirectories using flatMap
We have seen before how to list files in a specified directory. Let's take a look at how to traverse the direct subdirectories of the specified directory (depth is 1), first implement a simple version, and then use the more convenient flatMap() method to implement it.
We first use a traditional for loop to traverse a specified directory. If there are files in the subdirectory, add them to the list; otherwise, add the subdirectory to the list. Finally, print out the total number of all files. The code is below - this one is for hard mode.
Copy the code code as follows:
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())
}
We first get the file list in the current directory and then traverse it. For each file, if it has sub-files, add them to the list. There is no problem with this, but it has some common problems: mutability, basic type paranoia, imperativeness, code verbosity, etc. A small method called flatMap() can solve these problems.
As the name says, this method flattens after mapping. It maps elements in a collection just like map(). But unlike the map() method, the lambda expression in the map() method only returns an element, and what is returned here is a Stream object. So this method flattens multiple streams and maps each element inside to a flattened stream.
We can use flatMap() to perform various operations, but the problem at hand illustrates its value. Each subdirectory has a list or stream of files, and we want to get the list of files in all subdirectories under the current directory.
Some directories may be empty or have no child elements. In this case, we wrap the empty directory or file into a stream object. If we want to ignore a file, the flatMap() method in the JDK can also handle empty files very well; it will merge a null reference into the stream as an empty collection. Let’s look at the use of flatMap() method.
Copy the code code as follows:
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());
}
We first obtain the sub-file stream of the current directory, and then call its flatMap() method. Then pass a lambda expression to this method, which will return a stream of subfiles of the specified file. The flatMap() method returns a collection of files in all subdirectories of the current directory. We use the collect() method and the toList()( method in Collectors to collect them into a list.
The lambda expression we pass to flatMap() returns a subfile of a file. If not, the file's stream is returned. The flatMap() method elegantly maps this stream into a collection of streams, then flattens the collection and finally merges it into a single stream.
The flatMap() method reduces a lot of development work - it combines two consecutive operations, often called tuples - in one elegant operation.
We already know how to use the flatMap() method to list all files in an immediate subdirectory. Let's monitor the file modification operations.
Monitor file modifications
We already know how to find files and directories, but if we want to receive prompt messages when files are created, modified or deleted, this is also very simple. Such a mechanism is very useful for monitoring changes in special files such as configuration files and system resources. Let's explore this tool introduced in Java 7, WatchService, which can be used to monitor file modifications. Many of the features we see below come from JDK 7, but the biggest improvement here is the convenience brought by internal iterators.
Let's first write an example of monitoring file modifications in the current directory. The Path class in the JDK corresponds to an instance in the file system, which is a factory for observer services. We can register notification events for this service, like this:
Copy the code code as follows:
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...");
We registered a WatchService to observe modifications to the current directory. You can poll this WatchService to obtain the modification operations of files in the directory, and it will return these changes to us through a WatchKey. Once we have the key, we can loop through all its events to get the details of the file update. Because multiple files may be modified at the same time, the poll operation may return multiple events. Let’s take a look at the polling and traversal code.
Copy the code code as follows:
final WatchKey watchKey = watchService.poll(1, TimeUnit.MINUTES);
if(watchKey != null) {
watchKey.pollEvents()
.stream()
.forEach(event ->
System.out.println(event.context()));
}
As you can see here, the features of Java 7 and Java 8 appear at the same time. We convert the collection returned by pollEvents() into a Java 8 Stream, and then use its internal iterator to print out detailed update information for each file.
Let's run this code, and then modify the sample.txt file in the current directory to see if the program can detect this update.
Copy the code code as follows:
Report any file changed within next 1 minute...
sample.txt
When we modify this file, the program will prompt that the file has been modified. We can use this feature to monitor updates to different files and then perform corresponding tasks. Of course, we can also only register file creation or deletion operations.
Summarize
With lambda expressions and method references, common tasks like string and file manipulation, and creating custom comparators become easier and more concise. Anonymous inner classes become elegant, and variability disappears like morning mist after sunrise. Another benefit of coding in this new style is that you can use the JDK's new facilities to efficiently traverse large directories.
Now you know how to create a lambda expression and pass it to a method. In the next chapter, we will introduce how to use functional interfaces and lambda expressions to design software.