Chapter TWENTY-FOUR
NIO.2


Exam Objectives

Use Path interface to operate on file and directory paths.
Use Files class to check, read, delete, copy, move, manage metadata of a file or directory.

NIO.2

In the last chapter, we reviewed the classes of the java.io package.

In the first versions of Java, this package, especially the File class, provided support for file operations. However, it had some problems, like lacking functionality and limited file attribute support.

For that reason, Java 1.4 introduced the NIO (Non-blocking Input/Output) API in the package java.nio implementing new functionality like channels, buffering, and new charsets.

However, this API didn't entirely solve the problems with the java.io package, so in Java 7, the NIO.2 API was added in the java.nio.file package (actually, since this is a new package, NIO.2 is not an update to the NIO API, besides, they focus on different things).

NIO.2 provides better support for accessing files and the file system, symbolic links, interoperability, and exceptions among others.

The primary classes of java.nio.file, Path, Paths, and Files, are intended to provide an easier way to work with files and to be a replacement for the java.io.File class.

These classes will be the focus of this chapter. Let's start with Path and Paths.

The Path interface

In the previous chapter, we also reviewed concepts like file systems and paths.

Well, the Path interface defines an object that represents the path to a file or a directory.

When you think about the fact that a path varies between different file systems, it makes sense that Path is an interface. Thanks to this, Java transparently handles different implementations between platforms.

For example, here are some differences between Windows-based and Unix-based systems:

Since Path is an interface and Java handles its implementations, we have to use a utility class to create Path instances.

java.nio.files.Paths is this class. It provides two methods to create a Path object:

static Path get(String first, String... more)
static Path get(URI uri)

Be very careful with the names:

Path is the interface with methods to work with paths.

Paths is the class with static methods to create a Path object.

With the first version of Paths.get() you can create a Path object in these ways:

// With an absolute path in windows
Path pathWin = Paths.get("c:\\temp\\file.txt");
// With an absolute path in unix
Path pathUnix = Paths.get("/temp/file.txt");
// With a relative path
Path pathRelative = Paths.get("file.txt");
//Using the varargs parameter
// (the separator is inserted automatically)
Path pathByParts = Paths.get("c:", "temp", "file.txt");

With the second version, you have to use a java.net.URI instance. Since we're working with files, the URI schema must be file://:

try {
    Path fileURI = Paths.get(new URI("file:///c:/temp/file.txt"));
} catch (URISyntaxException e) {
    //This checked exception is thrown by the URI constructor
}

If you don't want to catch URISyntaxException, you can use the static method URI.create(String). It wraps the URISyntaxException exception in an IllegalArgumentException (a subclass of RuntimeException):

Path fileURI = Paths.get(URI.create("file:///c:/temp/file.txt"));

Notice the three slashes. file:/// represents an absolute path (the file:// schema plus another slash for the root directory). We can test this with the help of the toAbsolutePath() method, which returns the absolute path representation of a Path object:

Path fileURI = Paths.get(URI.create("file:///file.txt"));
System.out.println(fileURI.toAbsolutePath());

This will print either:

C:\file.txt // in Windows-based systems
/file.txt // Or in Unix-based systems

We can also create a Path from a File and vice-versa:

File file = new File("/file.txt");
Path path = file.toPath();

path = Paths.get("/file.txt");
file = path.toFile();

And just to make clear that the Path instance is system-dependent, let me tell you that Paths.get() is actually equivalent to:

Path path = FileSystems.getDefault().getPath("c://temp");

As you can see from the examples, the absolute path representation of a Path object has a root component (either c:\ or /) and a sequence of names separated by a (forward or backward) slash.

These names represent the directories needed to navigate to the target file or directory. The last name in the sequence represents the name of the target file or directory.

For example, the elements of the path c:\temp\dir1\file.txt (or its Unix equivalent, /temp/dir1/file.txt) are:

Root: c:\ (or /)
Name 1: temp
Name 2: dir1
Name 3: file.txt

The Path object has some methods to get this information. Except for toString() and getNameCount(), each of these methods returns a Path object):

Path path = Paths.get("C:\\temp\\dir1\\file.txt");
// Or Path path = Paths.get("/temp/dir1/file.txt");
System.out.println("toString(): " + path.toString());
System.out.println("getFileName(): " + path.getFileName());
System.out.println("getNameCount(): " +path.getNameCount());
// Indexes start from zero 
System.out.println("getName(0): " + path.getName(0));
System.out.println("getName(1): " + path.getName(1));
System.out.println("getName(2): " + path.getName(2));
// subpath(beginIndex, endIndex) from beginIndex to endIndex-1
System.out.println("subpath(0,2): " + path.subpath(0,2));
System.out.println("getParent(): " + path.getParent());
System.out.println("getRoot(): " + path.getRoot());

The output:

toString(): C:\temp\dir1\file.txt // Or /temp/dir1/file.txt
getFileName(): file.txt
getNameCount(): 3
getName(0): temp
getName(1): dir1
getName(2): file.txt
subpath(0,2): temp\dir1 // Or temp/dir1
getParent(): C:\temp\dir1 // Or /temp/dir1
getRoot(): C:\ // Or /

Passing an invalid index to getName() and subpath() will throw an IllegalArgumentException (a RuntimeException).

If the path is specified as a relative one (and assuming this code is executed from the c:\temp directory):

Path path = Paths.get("dir1\\file.txt");// Or dir1/file.txt 
System.out.println("toString(): " + path.toString());
System.out.println("getFileName(): " + path.getFileName());
System.out.println("getNameCount(): " + path.getNameCount());
System.out.println("getName(0): " + path.getName(0));
System.out.println("getName(1): " + path.getName(1));
System.out.println("subpath(0,2): " + path.subpath(0,2));
System.out.println("getParent(): " + path.getParent());
System.out.println("getRoot(): " + path.getRoot());

The output:

toString(): dir1\file.txt // Or dir1/file.txt
getFileName(): file.txt
getNameCount(): 2
getName(0): dir1
getName(1): file.txt
subpath(0,2): dir1\file.txt // Or dir1/file.txt
getParent(): dir1
getRoot(): null

When working with paths, you can use:

For example:

// refers to /temp/file.txt
Path p1 = Paths.get("/temp/./file.txt");  
// refers to /temp//file.txt
Path p2 = Paths.get( "/temp/dir1/../file.txt"); 

In these cases, you can use the normalize() method to remove redundancies like . and .. (in other words, to "normalize" it):

Path path = Paths.get("/temp/dir1/../file.txt");
System.out.println(path); // /temp/dir1/../file.txt
Path path2 = path.normalize();
System.out.println(path2); // /temp/file.txt

This method does not access the file system to know if a file exists, so removing .. and a preceding name from a path may result in a path that no longer references the original file. This can happen when that previous name is a symbolic link (a reference to another file).

It's better to use the toRealPath() method:

Path toRealPath(LinkOption... options) throws IOException

This method does the following:

We can combine two paths. There are two cases.

First case. If we have an absolute path and we want to combine it with a second path that doesn't have a root element (a partial path), the second path is appended:

Path path = Paths.get("/temp");
System.out.println(path.resolve("newDir")); // /temp/newDir

Second case. If we have a partial or relative path, and we want to combine it with an absolute path, this absolute path is returned:

Path path = Paths.get("newDir");
System.out.println(path.resolve("/temp")); // /temp

relativize() is another interesting method.

path1.relativize(path2) is like saying give me a path that shows how to get from path1 to path2.

For example, if we are in directory /temp and we want to go to /temp/dir1/subdir, we have to go first to dir1 and then to subdir:

Path path1 = Paths.get("temp");
Path path2 = Paths.get("temp/dir1/file.txt");
Path path1ToPath2 = path1.relativize(path2); // dir1/file.txt

If the paths represent two relatives paths without any other information, they are considered siblings, so you have to go to the parent directory and then go to the other directory:

Path path1 = Paths.get("dir1");
Path path1ToPath2 = path1.relativize(Paths.get("dir2")); // ../dir2

Notice that both examples use relative paths.

If one of the paths is an absolute path, a relative path cannot be constructed because of the lack of information and a llegalArgumentException will be thrown.

If both paths are absolute, the result is system-dependent.

Path implements the Iterable interface so you can do something like this:

Path path = Paths.get("c:\\temp\\dir1\file.txt");
for(Path name : path) {
    System.out.println(name);
}

The output:

temp
dir1
file.txt

Path implements the Comparable interface and the equals() method to test two paths for equality.

compareTo() compares two paths lexicographically. It returns:

The equals() implementation is system-dependent (for example, it's case insensitive on Windows systems). However, it returns false if the argument is not a Path or if it belongs to a different file system.

In addition, the methods startsWith() and endsWith() both test whether a path begins or ends with some String (in this case, the methods return true only if the string represents an actual element) or Path. So given:

Path absPath = Paths.get("c:\\temp\\dir1\\file.txt");
Path relPath = Paths.get("temp\\dir1\\file.txt");

boolean startsWith(Path other)
absPath.startsWith(Paths.get("c:\\temp\\file.txt")); // false
absPath.startsWith(Paths.get("c:\\temp\\dir1\\img.jpg")); // false
absPath.startsWith(Paths.get("c:\\temp\\dir1\\")) // true
absPath.startsWith(relPath); // false

boolean
startsWith(String other)
relPath.startsWith("t")
; // false
relPath.startsWith("temp"); // true
relPath.startsWith("temp\\d"); // false
relPath.startsWith("temp\\dir1"); // true

boolean
endsWith(Path other)
absPath.endsWith("file.txt")
; // true
absPath.endsWith("d:\\temp\\dir1\\file.txt"); // false
relPath.endsWith(absPath); // false

boolean
endsWith(String other)
relPath.endsWith("txt")
; // false
relPath.endsWith("file.txt"); // true
relPath.endsWith("\\dir1\\file.txt"); // false
relPath.endsWith("dir1\\file.txt"); // true

These methods don't take into account trailing separators, so if we have the Path temp/dir1, invoking, for example, endsWith() with dir1/, it returns true.

The Files class

The java.nio.file.Files class has static methods for common operations on files and directories. In contrast with the java.io.File class, all methods of Files work with Path objects (so don't confuse File and Files).

For example, we can check if a path actually exists (or doesn't exist) with the methods:

static boolean exists(Path path, LinkOption... options)
static boolean notExists(Path path, LinkOption... options)

If LinkOption.NOFOLLOW_LINKS is present, symbolic links are not followed (by default they are).

We can check if a path is readable (it's not if the file doesn't exist or if the JVM doesn't have the privileges to access it):

static boolean isReadable(Path path)

We can check if a path is writable (it's not if the file doesn't exist or if the JVM doesn't have the privileges to access it):

static boolean isWritable(Path path)

We can check if a file exists and is executable:

static boolean isExecutable(Path path)

Or even check if two paths refer to the same file (useful if one path represents a symbolic link). If both Path objects are equal then this method returns true without checking if the file exists:

static boolean isSameFile(Path path,
                          Path path2)
throws IOException

To read a file, we can load the entire file into memory (only useful for small files) with the methods:

static byte[] readAllBytes(Path path)
                              throws IOException
static List<String> readAllLines(Path path)
                              throws IOException
static List<String> readAllLines(Path path, Charset cs)
                              throws IOException

For example:

try {
    // By default it uses StandardCharsets.UTF_8
    List<String> lines = Files.readAllLines(
                             Paths.get("file.txt"));
    lines.forEach(System.out::println); }
} catch (IOException e) { /** */ }

Or to read a file in an efficient way:

static BufferedReader newBufferedReader(Path path)
        throws IOException
static BufferedReader newBufferedReader(Path path, Charset cs)
        throws IOException

For example:

Path path = Paths.get("/temp/dir1/files.txt");
// By default it uses StandardCharsets.UTF_8
try (BufferedReader reader = Files.newBufferedReader(path,
        StandardCharsets.ISO_8859_1)) {
    String line = null;
    while((line = reader.readLine()) != null)
        System.out.println(line);
} catch (IOException e) { /** ... */ }

The Files class has two methods to delete files/directories.

static void delete(Path path) throws IOException

It removes the file/directory or throws an exception if something fails:

try {
    Files.delete(Paths.get("/temp/dir1/file.txt"));
    Files.delete(Paths.get("/temp/dir1"));
} catch (NoSuchFileException nsfe) {
    // If the file/directory doesn't exists
} catch (DirectoryNotEmptyException dnee) {
    // To delete a directory, it must be empty,
    // otherwise, this exception is thrown
} catch (IOException ioe) {
    // File permission or other problems
}

The second method is:

static boolean deleteIfExists(Path path) throws IOException

This method returns true if the file was deleted or false if the file could not be removed because it did not exist, in other words, unlike the first method, this doesn't throw a NoSuchFileException (but it still throws a DirectoryNotEmptyException and an IOException for other problems):

try {
    Files.delete(Paths.get("/temp/dir1/file.txt"));
} catch (DirectoryNotEmptyException dnee) {
    // To delete a directory, it must be empty,
} catch (IOException ioe) {
    // File permission or other problems
}

To copy files/directories, we have the method:

static Path copy(Path source, Path target,
                 CopyOption... options)
throws IOException

It returns the path to the target file, and when copying a directory, its content won't be copied.

By default, the copy fails if the destination file already exists. Also, file attributes won't be copied, and when copying a symbolic link, its target will be copied.

We can customize this behavior with the following CopyOption enums:

Here's an example:

import static java.nio.file.StandardCopyOption. REPLACE_EXISTING;
...
try {
    Files.copy(Paths.get("in.txt"),
               Paths.get("out.txt"),
               REPLACE_EXISTING);
} catch (IOException e) { /** ... */ }

There are methods to copy between a stream and a Path also:

static long copy(InputStream in, Path target,
                 CopyOption... options)
throws IOException

Copies all bytes from an input stream to a file. By default, the copy fails if the target already exists or is a symbolic link. If the StandardCopyOption.REPLACE_EXISTING option is specified, and the target file already exists, then it is replaced if it's not a non-empty directory. If the target file exists and is a symbolic link, then the symbolic link is replaced. Actually, in Java 8, the REPLACE_EXISTING option is the only option required to be supported by this method.

static long copy(Path source,
                 OutputStream out)
throws IOException

Copies all bytes from a file to an output stream.

For example:

try (InputStream in = new FileInputStream("in.csv");
        OutputStream out = new FileOutputStream("out.csv")) {
    Path path = Paths.get("/temp/in.txt");
    // Copy stream data to a file
    Files.copy(in, path);
    // Copy the file data to a stream
    Files.copy(path, out);
} catch (IOException e) { /** ... */ }

To move or rename a file/directory, we have the method:

static Path move(Path source, Path target,
                 CopyOption... options)
throws IOException

By default, this method will follow links, throw an exception if the file already exists, and not perform an atomic move.

We can customize this behavior with the following CopyOption enums:

This method can move a non-empty directory. However, if the target exists, trying to move a non-empty directory will throw a DirectoryNotEmptyException. This exception will also be thrown when trying to move a non-empty directory across a drives or partitions.

For example:

try {
    // Move or rename dir1 to dir2
    Files.move(Paths.get("c:\\temp\\dir1|"),
               Paths.get("c:\\temp\\dir2");
} catch (IOException e) { /** ... */ }

Managing metadata

When talking about a file system, metadata give us information about a file or directory, like its size, permissions, creation date, etc. This information is referred as attributes, and some of them are system-dependent.

The Files class has some methods to get or set some attributes from a Path object:

static long size(Path path) throws IOException

Returns the size of a file (in bytes).

static boolean isDirectory(Path path, LinkOption... options)

Tests whether a file is a directory.

static boolean isRegularFile(Path path, LinkOption... options)

Tests whether a file is a regular file.

static boolean isSymbolicLink(Path path)

Tests whether a file is a symbolic link.

static boolean isHidden(Path path) throws IOException

Tells whether a file is considered hidden.

static FileTime getLastModifiedTime(Path path,
            LinkOption... options)
throws IOException
static Path setLastModifiedTime(Path path,
            FileTime time)
throws IOException

Returns or updates a file's last modified time.

static UserPrincipal getOwner(Path path,
            LinkOption... options)
throws IOException
static Path setOwner(Path path,
            UserPrincipal owner)
throws IOException

Returns or updates the owner of the file.

In methods that take an optional LinkOption.NOFOLLOW_LINKS, symbolic links are not followed (by default they are).

In the case of getLastModifiedTime() and setLastModifiedTime() the class java.nio.file.attribute.FileTime represents the value of a file's time stamp attribute.

We can create an instance of FileTime with these static methods:

static FileTime from(Instant instant)
static FileTime from(long value, TimeUnit unit)
static FileTime fromMillis(long value)

And from a FileTime we can get an Instant or milliseconds as long:

Instant toInstant()
long toMillis()

For example:

try {
    Path path = Paths.get("/temp/dir1/file.txt");
    FileTime ft = Files.getLastModifiedTime(path);
    Files.setLastModifiedTime(path,
        FileTime.fromMillis(ft.toMillis + 1000)); // adds a second
} catch (IOException e) { /** ... */ }

In the case of getOwner() and setOwner() the interface java.nio.file.attribute.UserPrincipal is an abstract representation of an identity that can be used like this:

try {
    Path path = Paths.get("/temp/dir1/file.txt");
    // FileSystems.getDefault() also gets a FileSystem object
    UserPrincipal owner = path.getFileSystem()
                .getUserPrincipalLookupService()
                .lookupPrincipalByName("jane");
    Files.setOwner(path, owner);
} catch (IOException e) { /** ... */ }

These methods are useful to get or update a single attribute. But we can also get a group of related attributes by functionality or by a particular systems implementation as a view.

The three most common view classes are:

You can get a file attribute view of a given type to read or update a set of attributes with the method:

static <V extends FileAttributeView> V getFileAttributeView(
            Path path, Class<V> type,LinkOption... options)

For example, BasicFileAttributeView has only one update method:

try {
   Path path = Paths.get("/temp/dir/file.txt");
   BasicFileAttributeView view =
      Files.getFileAttributeView(path,
                 BasicFileAttributeView.class);
   // Get a class with read-only attributes
   BasicFileAttributes readOnlyAttrs =
                   view.readAttributes();
   FileTime lastModifiedTime =
                   FileTime.from(Instant.now());
   FileTime lastAccessTime =
                   FileTime.from(Instant.now());
   FileTime createTime =
                   FileTime.from(Instant.now());
   //If any argument is null,
   //the corresponding value is not changed

   view.setTimes(lastModifiedTime,
                 lastAccessTime,
                 createTime);
} catch (IOException e) { /** ... */ }

Most of the time, you'll work with the read-only versions of the file views. In this case, you can use the following method to get them directly:

static <A extends BasicFileAttributes> A
   readAttributes(Path path, Class<A> type,
                  LinkOption... options)

        throws IOException

The second parameter is the return type of the method, the class that contains the attributes to use (notice that all attributes classes extend from BasicFileAttributes because it contains attributes common to all file systems). The third argument is when you want to follow symbolic links.

Here's an example of how to access the file attributes of a file using the java.nio.file.attribute.BasicFileAttributes class:

try {
   Path path = Paths.get("/temp/dir1/file.txt");
   BasicFileAttributes attr = Files.readAttributes(
         path, BasicFileAttributes.class);
   // Size in bytes
   System.out.println("size(): " + attr.size());
   // Unique file identifier (or null if not available)
   System.out.println("fileKey(): " + attr.fileKey());

   System.out.println("isDirectory(): " + attr.isDirectory());
   System.out.println("isRegularFile(): " + attr.isRegularFile());
   System.out.println("isSymbolicLink(): " + attr.isSymbolicLink());
   // Is something other than a file, directory, or symbolic link?
   System.out.println("isOther(): " + attr.isOther());

   // The following methods return a FileTime instance

   System.out.println("creationTime(): " + attr.creationTime());
   System.out.println("lastModifiedTime():"+attr.lastModifiedTime());
   System.out.println("lastAccessTime(): " + attr.lastAccessTime());
} catch (IOException e) { /** ... */ }

Key Points

Self Test

1. Given:

Path path1 = Paths.get("/projects/work/../fun");
Path path2 = Paths.get("games");
System.out.println(path1.resolve(path2));

Which of the following is the result of executing the above lines?
A. /project/work/fun/games
B. /project/fun/games
C. /project/work/../fun/games
D. games

2. Given:

Path path = Paths.get("c:\\Users\\mark");

Which of the following will return Users?
A. path.getRoot()
B. path.getName(0)
C. path.getName(1)
D. path.subpath(0, 0);

3. Which of the following is not a valid CopyOption for Files.copy()?
A. NOFOLLOW_LINKS
B. REPLACE_EXISTING
C. ATOMIC_MOVE
D. COPY_ATTRIBUTES

4. Given:

Path path =
  Paths.get("c:\\.\\temp\\data\\..\\.\\dir\\..\\file.txt");
try {
   path = path.toRealPath();
} catch (IOException e) { }
System.out.println(path.subpath(1,2));

Which is the result?
A. temp
B. data
C. dir
D. file.txt

5. Which of the following is a valid way to set a file's create time?

A.

FileTime time = FileTime.from(Instance.now());
Files.getFileAttributeView(path,
      BasicFileAttributeView.class)
   .setTimes(null, time, null);

B.

Files.setCreateTime(path,
   FileTime.from(Instance.now());

C.

Files.getFileAttributeView(path,
      BasicFileAttributeView.class)
   .setTimes(null, null, Instance.now());

D.

FileTime time = FileTime.from(Instance.now());
Files.getFileAttributeView(path,
      BasicFileAttributeView.class)
   .setTimes(null, null, time);