Java functional programming of twelve: monitoring file modifications

  • 2020-04-01 03:30:38
  • OfStack

Use flatMap to list subdirectories

You've already seen how to list files in a specified directory. Let's look again at how to traverse the direct subdirectory of a given directory (depth 1), implement a simple version first, and then implement it using the more convenient flatMap() method.

We first use the traditional for loop to walk through a specified directory. If there are files in the subdirectory, add them to the list. Otherwise, add subdirectories to the list. Finally, print out the total of all the files. The code is below -- this one is in hard mode.


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 list of files in the current directory, and then we iterate. For each file, if it has children, add them to the list. This is fine, but it has some common problems: variability, base type paranoia, imperative, code verbosity, and so on. A small method called flatMap() solves these problems.

As the name suggests, this method is flattened after mapping. It maps the elements in the collection like a map(). But unlike the map() method, the lambda expression in the map() method simply returns an element, whereas here it returns a Stream object. This method then flattens the multiple streams, mapping each element inside to a flat 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 a list of files in all of the subdirectories in 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 also works well with empty files; It will merge an empty reference into the stream as an empty collection. Consider the use of the flatMap() method.


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 get the substream of the current directory and then call its flatMap() method. A lambda expression is then passed to the method, which returns the stream of subfiles of the specified file. The flatMap() method returns a collection of files in all subdirectories of the current directory. We collect them into a list using the collect() method and the toList() method in the Collectors.

This lambda expression we pass to flatMap() returns a subfile of a file. If not, the stream of the file is returned. The flatMap() method elegantly maps the stream to a collection of streams, then flattens the collection and finally merges it into a stream.

The flatMap() method takes a lot of development work out of the way -- it combines two consecutive operations together nicely, often called tuples -- and does it in one elegant operation.

We've learned how to use the flatMap() method to list all the files in a direct subdirectory. Now let's monitor the changes to the file.

Monitor file modification

We already know how to find files and directories, but this is easy if we want to be prompted when a file is created, modified, or deleted. This mechanism is useful for monitoring changes to special files such as configuration files and system resources. Let's explore the tool introduced in Java 7, WatchService, which can be used to monitor file changes. Many of the features we'll see below come from JDK 7, and the biggest improvement here is the convenience of the internal iterator.

Let's start with an example of monitoring file changes in the current directory. The Path class in the JDK corresponds to an instance in the file system, which is a factory for the observer service. We can register notification events for this service, like this:


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 changes to the current directory. You can poll the WatchService for changes to files in the directory, and it returns them to us with a WatchKey. Once we have the key, we can go through all of its events to get the details of the file updates. Because multiple files may be modified at the same time, the poll operation may return multiple events. Look at the code for polling and traversing.


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, both Java 7 and Java 8 features are coming out at the same time. We converted the collection returned by pollEvents() into a Java 8 Stream and used its internal iterator to print out the detailed updates for each file.

Let's run this code and modify the sample.txt file in the current directory to see if the program can detect the update.


Report any file changed within next 1 minute... sample.txt

When we modify the file, the program will prompt that the file has been modified. We can use this feature to monitor the updates of different files and then perform the corresponding tasks. Of course, we can only register the operation of file creation or deletion.

conclusion

With lambda expressions and method references, common tasks like string and file operations, creating custom comparators, become much simpler and more concise. Anonymous inner classes became elegant, and variability disappeared like the morning fog 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. The next chapter shows how to use functional interfaces and lambda expressions for software design.


Related articles: