Directory listing with the NIO.2 API

The NIO.2 (“New Input/Output 2”) is a new API introduced in JDK 7 to operate more efficiently and uniformly on file systems. This new API covers many file and directory-specific aspects, such as handling attributes and symbolic links. This article discusses one of the most straightforward aspects of NIO.2, listing directory entries using the new DirectoryStream interface.

Directory listing before JDK 7

Before JDK 7, the only way to list directory entries was through the list and listFiles methods of the well-known java.io.File class.

public String[] list()
public String[] list(FilenameFilter filter)
public File[] listFiles()
public File[] listFiles(FileFilter filter)
public File[] listFiles(FilenameFilter filter)

These five methods aren’t evil by themselves. However, they have some drawbacks. First of all, they are not particularly efficient when a directory contains thousands and thousands of entries. Indeed, these methods must scan the entire directory before returning the entries array. Another drawback is that any globbing logic (filtering entries matching a pattern like A*.jpeg) is not easy to implement and requires at least a custom implementation of either FileFilter or FilenameFilter interface.

Directory listing since JDK 7

The NIO.2 API provides a new interface called DirectoryStream. It represents a new way to enumerate directory entries efficiently and practically. DirectoryStream has some notable features:

1) It is a generic interface. However, the concrete parameterization used in the standard framework is always <Path> (java.nio.file.Path), not something else.

2) It implements the Iterable<T> interface. A DirectoryStream object can be iterated using the enhanced-for (“for-each”) loop available since Java 5. Note that DirectoryStream is not a general-purpose implementation of Iterable. You can perform only one iteration on a DirectoryStream object. Any further implicit or explicit invocation of the iterator() method throws IllegalStateException.

3) It implements both the Closeable and the AutoCloseable interface. A DirectoryStream must be closed to release all the resources associated with the stream. The best way to ensure proper closure of the stream is with the try-with-resource statement, which was also introduced in Java 7.

Obtaining a DirectoryStream

Since DirectoryStream is an interface, you need to obtain a concrete implementation to do something useful. The java.nio.file.Files class contains 3 static “factory” methods to open a directory stream:

public static DirectoryStream<Path> newDirectoryStream(Path dir)
                                                throws IOException

public static DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter)
                                                throws IOException

public static DirectoryStream<Path> newDirectoryStream(Path dir, String glob)
                                                throws IOException

The first version is the simplest since it iterates over all directory entries without filtering. The second version receives a DirectoryStream.Filter. This functional interface works as a specialized “predicate” to decide whether an entry should be accepted.

The third version with the String glob parameter is the most interesting. A glob pattern can contain special characters like “?” and “*” to match more directory entries. Examples of glob patterns are A*.jpeg or config.* or report-0??.doc. Note that all major Operating Systems (Windows/Unix/Mac) support some form of globbing.

The glob syntax of the NIO.2 API is specified and documented by the getPathMatcher method in the FileSystem class. If you look at the linked Javadoc, you can notice that the glob syntax supports some unusual forms. In particular, a form like *.{java,class} is not natively available on all Operating Systems. But fortunately, the NIO.2 API manages it uniformly on all systems, even Windows systems (where this form is not natively available).

Usage examples

The following are just two simple examples of the use of DirectoryStream. The first example uses a custom filter, while the second uses a glob pattern.

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class NioDirList1 {
    public static void main(String[] args) {
        Path dirPath = Paths.get(".");    // current directory as example

        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dirPath,
                path -> isWritableRegularFile(path))) {
            for (Path entry : dirStream) {
                System.out.println("Found: " + entry);
            }
        } catch (IOException e) {
            System.err.println(e);
        }
    }

    private static boolean isWritableRegularFile(Path path) {
        return Files.isRegularFile(path) && Files.isWritable(path);
    }
}
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class NioDirList2 {
    public static void main(String[] args) {
        Path dirPath = Paths.get(".");    // current directory as example

        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(dirPath, "*.java")) {
            for (Path entry : dirStream) {
                System.out.println("Found: " + entry);
            }
        } catch (IOException e) {
            System.err.println(e);
        }
    }
}

Is globbing case-sensitive?

This is a good question. Unfortunately, the Javadoc documentation of getPathMatcher is a bit vague about this aspect of globbing:

[…] For both the glob and regex syntaxes, the matching details, such as whether the matching is case sensitive, are implementation-dependent and therefore not specified.

So I decided to do a test on two machines I own, one with Microsoft Windows (Windows 10) and the other with a Linux distribution (Xubuntu 22.04). On both systems, I have created the following 3 (empty) files in a test directory:

  • one.txt
  • two.TXT
  • three.TxT

And then I tried the following code in that directory:

import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SecureDirectoryStream;

public class GlobCaseTest {
    public static void main(String[] args) throws IOException {
        try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(Paths.get("."), "*.txt")) {
            dirStream.forEach(System.out::println);
        }
    }
}

The following is the output on Windows 10:

.\one.txt
.\three.TxT
.\two.TXT

The following is the output on Xubuntu 22.04:

./one.txt

So the bottom line about globbing is:

  • It is case-insensitive on Windows machines.
  • It is case-sensitive on Linux machines.

Some words about SecureDirectoryStream

If you look at the Javadoc documentation of the java.nio.file package, you should notice another interface called SecureDirectoryStream, a sub-interface of DirectoryStream.

This SecureDirectoryStream interface performs the operations safely to avoid race conditions. For example, imagine that you want to delete all the files in a directory and that, while iterating on the DirectoryStream, “someone” moves that directory elsewhere. What happens? If you use the deleteFile method of SecureDirectoryStream, you can ensure that the delete operation works correctly, even in this particular scenario.

Note that you cannot explicitly request a SecureDirectoryStream. If the underlying Operating System supports “secure” operations, then the Files.newDirectoryStream methods return an object that implements SecureDirectoryStream, not just only DirectoryStream. So you have to do something like this:

try (DirectoryStream<Path> dirStream = Files.newDirectoryStream( /*.....*/ )) {
    if (dirStream instanceof SecureDirectoryStream) {
        SecureDirectoryStream<Path> secDirStream = (SecureDirectoryStream<Path>) dirStream;

        // Ok, we can perform “secure” operations ...
    }
}

From what I have tested so far, I found that SecureDirectoryStream is supported on Xubuntu but not on Windows 10.

Similar Posts