Introduction
In the last post, we looked at the Java NIO Files API where we learnt the Files methods that deal with creating, reading, writing, and deleting files. In this post, we will learn about the NIO Files Directory operations i.e., directory manipulations.
I have structured this post as
- Testing if an object pointed to by a path is a directory
- Creating a directory and creating multiple directories
- Creating a temp directory
- Obtaining a Directory Stream
- Recursively traversing a directory
- Moving a directory
Note: All the Files methods throw an IOException which is common if we are working on a file or a directory. Hence, we have to either catch the IOException or make our method (that calls the Files method) throw it back since it is a checked exception (by adding a throws clause).
A Path is an object that locates a file in the file system. Look at the section on Java NIO Path as the methods in the Files class operate using a Path.
How to check if a file is a directory
We can use the isDirectory() method of the Files class to check if the file pointed to by a Path is a directory. It returns true if the file is a directory and false otherwise.
Path directoryPath = Paths.get("/Users/JavaDeveloperCentral/data/files");
System.out.println(Files.isDirectory(directoryPath));//true
Path filePath = Paths.get("/Users/JavaDeveloperCentral/data/files/file.txt");
System.out.println(Files.isDirectory(filePath));//false
The isDirectory method accepts a var-args parameter of type LinkOption. A LinkOption has an enum instance called NOFOLLOW_LINKS. By default, if the file is symbolic link, it is followed. We can pass the LinkOption as an argument to not follow symbolic links.
System.out.println(Files.isDirectory(directoryPath, LinkOption.NOFOLLOW_LINKS));
Creating a directory using NIO Files
Use Files.createDirectory() method to create a directory. The method returns the path of the newly created directory.
Path newDirectoryPath = Paths.get("/Users/JavaDeveloperCentral/data/newDirectory");
System.out.println(Files.createDirectory(newDirectoryPath));
The above will create a new folder or directory named ’newDirectory’ inside the ‘data’ directory. If the directory already exists (even if it is a file), the createDirectory() method will throw a FileAlreadyExistsException.
But we can use it to create only one new folder or directory. What do I mean by this?
In the above code snippet, the parent directory path up to the folder ‘data’ must already exist. For instance, if the ‘data’ directory does not exist, it will throw a NoSuchFileException.
Path newDirectoryPath = Paths.get("/Users/JavaDeveloperCentral/data/newDir1/newDir2");
Files.createDirectory(newDirectoryPath); //throws a NoSuchFileException
Creating all missing directories
It would be painful if we have to create the entire directory path (all non-existent parent directories) ourselves. There is a method that does this for us - the createDirectories method.
The createDirectories method will create all non-existent parent directories first.
Path newDirectoryPath = Paths.get("/Users/JavaDeveloperCentral/data/newDir1/newDir2");
Files.createDirectories(newDirectoryPath);
The above code will create a directory structure like
..
|-- data
|-- newDir1*
|-- newDir2*
* - newly created
Note: The createDirectories method will not throw a FileAlreadyExistsException if the directory with that name already exists.
The createDirectory and the createDirectories method takes an optional FileAttribute (var-args). They are used to set the file attributes. The usage of FileAttribute is shown on the section for creating files.
Creating a temp directory
The createTempDirectory method accepts a path to a directory and a prefix, and it creates a new temp directory in the specified directory using the provided prefix to generate the name of the temp directory. It returns the Path instance of the newly created directory.
The name of the created temp directory will be of the format <prefix><some_name> where the name format of the <some_name> is implementation dependent and cannot be specified. The current implementation is to generate a random long as the directory name with the provided prefix separated by a colon i.e., like <prefix>-<some_long>. If the prefix is empty the directory name will only be the randomly generated Long (as a String).
Path directoryPath = Paths.get("/Users/JavaDeveloperCentral/data");
//Note: data directory has to exit
Path tmpDirPath = Files.createTempDirectory(directoryPath, "prefix-");
System.out.println(tmpDirPath);
This will create a temp directory inside the data directory. The returned directory’s path is like /Users/JavaDeveloperCentral/data/prefix-17664530745782273920
where the last long String is randomly generated.
A temp directory inside the temporary-file directory
In the above createTempFile method we passed a Path directory in which we want to create a temp directory. There is another overloaded createTempFile method where we can skip providing the Path directory. In that case, it will create the temp directory inside the temporary-file directory.
The temporary-file directory path is OS specific.
System.out.println(System.getProperty("java.io.tmpdir"));
On windows, it might be C:\\temp
and in Mac it might be /var/folders/qx/045nmsxx55575\_gqycvvs1480000gp/T
. The exact path can change.
The usage of the method is shown below
/*
Creates the temp dir inside the temporary-file directory (location is OS specific).
*/
System.out.println(Files.createTempDirectory("prefix-"));
Note: We can pass the optional list of file attributes as a var-args to the createTempDirectory method.
When to use createTempDirectory
The need for creating a temporary directory is when you want to write some data within a directory and you want to clean up (delete) the directory after completing the processing. Hence, you do not care what the directory is called.
Example: A report processing application might generate multiple report files. It writes the report data to the local file system. After it is has completed generating the reports, it will upload the directory containing report files to an external store (like AWS S3). Then, it wants to delete all the report files (including the directory). The application, if it uses a temp directory created as explained above, need not worry about naming the directory. In such applications, we can use the second variation of creating a temporary directory (create a temp directory in the temporary-file directory)
But still a temporary directory removes the need for the application to provide a name. It would not be deleted automatically.
A temporary directory is usually used along with a shutdown-hook or File.deleteOnExit() to delete the directory automatically. The deleteOnExit() will delete the file after the Virtual Machine terminates.
tmpDirPath.toFile().deleteOnExit();
For the directory to be deleted when the VM exits, the directory must be empty. This means that all the files (and folders) created inside the temp directory must also have to be deleted on exit.
Here’s an example,
Path directoryPath = Paths.get("/Users/JavaDeveloperCentral/data");
Path tmpDirPath = Files.createTempDirectory(directoryPath, "");
System.out.println(tmpDirPath);
tmpDirPath.toFile().deleteOnExit();
Path filePath1 = Paths.get(tmpDirPath.toString() + "/file1");
filePath1.toFile().deleteOnExit();
Files.write(filePath1, List.of("something"));
Path filePath2 = Paths.get(tmpDirPath.toString() + "/file2");
filePath2.toFile().deleteOnExit();
Files.write(filePath2, List.of("something"));
When the JVM terminates, the files, file1, file2 and the temp directory that have them will all be deleted automatically.
Directory Stream
The newDirectory stream method opens a directory and returns a DirectoryStream. A DirectoryStream can be used to iterate over all the entries in the directory. Each entry in a DirectoryStream is a NIO Path.
Some Related posts to the Iterator
The stream must be closed to free any resources held for the open directory. We can use a try-with-resources statement to ensure the directory stream is closed.
Let us say we have a directory structure as
..
|-- data
|-- file1.txt
|-- file2.txt
|-- SomeClass.java
|-- dir1
|-- dir2
|-- file1InDir2.txt
|-- file3.txt
Path directoryPath = Paths.get("/Users/JavaDeveloperCental/data");
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directoryPath)) {
directoryStream
.forEach(System.out::println);
}
The above code creates a Directory Stream and iterates over them and prints each path.
The output would be:
/Users/JavaDeveloperCental/data/file1.txt
/Users/JavaDeveloperCental/data/file2.txt
/Users/JavaDeveloperCental/data/SomeClass.java
/Users/JavaDeveloperCental/data/dir1
/Users/JavaDeveloperCental/data/file3.txt
It can be seen that it iterates over the entries inside a directory, but it does not recursively traverse the nested directories. In the above example, it does not traverse inside the directory named dir1 and hence it does not print dir2 or file1InDir2.txt’s Path.
We can use the Files.walk method to recursively walk over the entire tree. We will explore this in the next section of this post.
Shown below is the equivalent of the above code but using explicitly the Iterator’s methods (hasNext() and next())
Iterable<Path> directoryStream = Files.newDirectoryStream(directoryPath);
Iterator<Path> pathIterator = directoryStream.iterator();
while (pathIterator.hasNext()) {
System.out.println(pathIterator.next());
}
- A DirectoryStream is an Iterable of Paths. But, it cannot be used as a general purpose Iterable as it supports only a single Iterator. We cannot invoke the iterator() method to obtain a second iterator. It will throw an IllegalStateException if we do so.
- Calling the hasNext() reads ahead by at least one element. So, if hasNext() returns true, next() is guaranteed to return a result.
- If an I/O error is encountered, then the hasNext/next methods will throw a DirectoryIteratorException.
- The order in which the directory entries are returned is not specified.
Filtering with a Directory Stream
There is an overloaded newDirectoryStream method that takes a globbing pattern. Hence, it returns only the Path entries that match the String representation of the file name against the passed pattern.
The globbing pattern is specified by the getPathMatcher method. Read the javadoc of the getPathMatcher to understand the syntax of the glob pattern.
Path directoryPath = Paths.get("/Users/JavaDeveloperCental/data");
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directoryPath, "*.txt")) {
directoryStream
.forEach(System.out::println);
}
By specifying a glob pattern of *.txt, we filter only the text files. Running the above code for the directory structure shown above will print,
/Users/JavaDeveloperCental/data/file1.txt
/Users/JavaDeveloperCental/data/file2.txt
/Users/JavaDeveloperCental/data/file3.txt
This will not print the folder dir1 and SomeClass.java as they don’t match the glob pattern.
Filtering based on the Path instance
If we want to have a filtering condition based on the Path object, we can pass a Predicate using DirectoryStream.Filter. It has the same signature as a Java Predicate but throws an IOException.
Example: To iterate over a directory but filter only the files, we can do like
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directoryPath,
path -> path.toFile().isFile())) {
directoryStream
.forEach(System.out::println);
}
Prints,
/Users/JavaDeveloperCental/data/file1.txt
/Users/JavaDeveloperCental/data/file2.txt
/Users/JavaDeveloperCental/data/SomeClass.java
/Users/JavaDeveloperCental/data/file3.txt
path -> path.toFile().isFile()
is a lambda of target type DirectoryStream.Filter. The entries in the iterator that match this predicate alone will be returned.
Files.Walk
The walk method returns a Stream that is lazily populated with Path by walking the file tree starting from a given Path. The file tree is traversed depth-first and hence this method can be used to traverse the directory structure recursively by visiting all the nodes (files and folders).
During the traversal, if it encounters a directory, it opens it and visits all the files in the directory. When all the entries inside a directory have been visited, the directory is closed. The file tree walk continues at the next sibling of the directory.
To ensure proper cleanup of file system resources, this must be used from with a try-with-resources construct.
It accepts a FileVisitOption argument. By default, the symbolic links are not followed. To follow symbolic links, we can pass FileVisitOption.FOLLOW_LINKS
Path directoryPath = Paths.get("/Users/JavaDeveloperCental/data");
try(Stream<Path> pathStream = Files.walk(directoryPath)) {
pathStream
.forEach(System.out::println);
}
The above code starts the traversal starting at the path /Users/JavaDeveloperCental/data/
(refer to the earlier provided directory structure) and prints each path.
/Users/JavaDeveloperCental/data/file1.txt
/Users/JavaDeveloperCental/data/file2.txt
/Users/JavaDeveloperCental/data/SomeClass.java
/Users/JavaDeveloperCental/data/dir1
/Users/JavaDeveloperCental/data/dir1/dir2
/Users/JavaDeveloperCental/data/dir1/dir2/file1InDir2.txt
/Users/JavaDeveloperCental/data/file3.txt
There is an overloaded walk method to which we can pass a maxDepth parameter that limits the maximum number of levels of directories to visit. A value of 0 means that only the starting file is visited.
By default, it would assume a maxDepth of Integer.MAX_VALUE and hence the entire tree will be traversed.
Path directoryPath = Paths.get("/Users/JavaDeveloperCental/data");
try(Stream<Path> pathStream = Files.walk(directoryPath, 1)) {
pathStream
.forEach(System.out::println);
}
Since we have limited to a max depth of 1, it only traverses the nodes that are at the next level of data directory
/Users/JavaDeveloperCental/data/file1.txt
/Users/JavaDeveloperCental/data/file2.txt
/Users/JavaDeveloperCental/data/SomeClass.java
/Users/JavaDeveloperCental/data/dir1
/Users/JavaDeveloperCental/data/file3.txt
Moving or renaming a directory
The Files.move() method will also work for moving a directory. But it shouldn’t require moving the entries in the directory. This is true if we are moving/renaming the directory on the same FileStore.
Quoting from the javadoc of Files.move
When invoked to move a directory that is not empty then the directory is moved if it does not require moving the entries in the directory. For example, renaming a directory on the same
FileStore
will usually not require moving the entries in the directory. When moving a directory requires that its entries be moved then this method fails (by throwing anIOException
). To move a file tree may involve copying rather than moving directories and this can be done using thecopy
method in conjunction with theFiles.walkFileTree
utility method.
Path path1 = Path.of("/Users/JavaDeveloperCental/data");
Path path2 = Path.of("/Users/JavaDeveloperCental/newData");
//Works only if on the same filestore - else use walkFileTree
Files.move(path1, path2);
Directory deletion and copying
We cannot use Files.delete() method to delete a directory. It would work only if the directory is empty. If the directory is not empty, it will throw a DirectoryNotEmptyException.
Similarly, Files.copy() method does not recursively copy all the files and folders inside a directory. It only copies and creates an empty directory at the destination.
To achieve these two functionalities, we have to use the walkFileTree method. We will learn about this in a separate post which I will put up soon.
Conclusion
This post covered the basic directory operations and manipulations using the static utility methods in the Files class.
Stay tuned for the post on walkFileTree method.