Directory listing with the NIO.2 API

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

Directory listing before Java 7

Before Java 7 the only way to list directory entries was through the list and listFiles methods of the 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 really bad, however they have some drawbacks. First of all, they are not very efficient when a directory contains thousands and thousands of entries. 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 Java 7

The NIO.2 API provides a new interface called DirectoryStream. It represents the new way to enumerate directory entries in an efficient and practical way. DirectoryStream has some notable features:

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

2) It implements the Iterable<T> interface. This means that 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 a single iteration on a DirectoryStream object, and any further implicit or explicit invocation of the iterator() method throws IllegalStateException.

3) It implements both the Closeable and (implicitly) the AutoCloseable interface. A DirectoryStream needs to be closed to release all the resources associated with the stream. The best way to ensure proper close 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 in order 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 one, it iterates over all directory entries. The second version receives a DirectoryStream.Filter. This is a functional interface that works as predicate to decide if an entry should be accepted or not.

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

The glob syntax supported by 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 special forms. In particular, the form like *.{java,class} is generally not natively available on all Operating Systems but the NIO.2 API manages it uniformly on all systems, even on Windows systems (where this form is not natively available).

Examples of use

The following are just two simple examples on 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. 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 little test on two of my machines, one with Microsoft Windows (Windows 10) and the other with Linux (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 have tried the following short code in that same 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 output on Windows 10:

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

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 that there is another interface called SecureDirectoryStream which is a sub-interface of DirectoryStream.

This SecureDirectoryStream interface is used to perform some operations in a “safe” way to avoid race conditions. Imagine that you want to delete all the files in a directory and that, while you are iterating on the DirectoryStream, “someone” moves that directory elsewhere. What happens? If you use the deleteFile method of SecureDirectoryStream you can assure that the delete operation works correctly even in this particular scenario.

Note that you cannot explicitly request a SecureDirectoryStream. If “secure” operations are supported by the underlying Operating System, then the Files.newDirectoryStream methods return an object that is a DirectoryStream and also a SecureDirectoryStream. 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 ...
    }
}

For what I have tested so far, it seems that SecureDirectoryStream is supported on Xubuntu but not on Windows 10.

Similar Posts