Java NIO Files – Part I

Introduction

The java.nio package defines interfaces and classes for operating with files and file systems. The Java NIO Files class has static utility methods that operate on files, directories and other types of files. In this post, we will learn about the Java NIO Files API. It is under the java.nio.file package.

This post will cover the NIO Files class regarding operations on Files. A separate post will be put up for operating on folders. This post is structured in the following flow.

  • Quick introduction to the NIO Path class
  • Creating a file
  • Reading a file
  • Writing to a file
  • Copying a file from a location to another location
  • Moving/Renaming a file
  • Deleting a file
  • Checking if a file exists

Note that all the Files methods throw an IOException which is common if we are working on a file. Hence, we have to either catch the IOException or make the method (that calls the Files method) throw it back since it is a checked exception. This post explains the differences between a checked and an unchecked exception.

Java NIO Path

The java.nio.file’s Path class represents an object that locates a file in the file system. The APIs in the NIO Files class work using this class.

Constructing a Path

In Java 8

Use the Paths.get() static method to build a path instance.

Path path = Paths.get("/Users/JavaDeveloperCentral/data/files/file.txt");

This will create a Path object that locates the file provided as arguments. 

To avoid hard-coding the path separator (the path separator is OS/platform specific), we can pass the individual path parts to the of() method. The last argument is a var-args.

Path path = Paths.get("/Users", "JavaDeveloperCentral", "data", "files", "file.txt");

Another way to create a path is from a URI.

Path path = Paths.get(URI.create("file:///Users/JavaDeveloperCentral/data/files/file.txt"));

From Java 11 onwards

From Java 11 onwards, Paths.get is not recommended to be used. Instead, the recommendation is to use the static of() methods in the Path interface. It has the same contract as the Paths.get methods.

Path path = Path.of("/Users/JavaDeveloperCentral/data/files/file.txt");
Path path = Path.of("/Users", "JavaDeveloperCentral", "data", "files", "file.txt");
Path path = Path.of(URI.create("file:///Users/JavaDeveloperCentral/data/files/file.txt"));

java.nio.file.Path Methods

We will not go in depth to see all the methods in the Path class. I will show just a few. Refer to the Javadoc of the Path class to know more if needed.

toFile()

We can get a File instance from a Path by calling the toFile() method.

Path path = Paths.get("/Users", "JavaDeveloperCentral", "data", "files", "file.txt")
File file = path.toFile();

getFileName()

The getFileName method returns the name of the file, i.e., the last part of the path.

System.out.println(path.getFileName()); //file.txt

toUri()

Returns a URI that represents the path.

System.out.println(path.toUri()); //file:///Users/JavaDeveloperCentral/data/files/file.txt

getParent()

This method is opposite of getFileName() method. The getParent() returns the parent path (or null if the parent path is not present).

System.out.println(path.getParent());///Users/JavaDeveloperCentral/data/files

Creating a file using NIO Files

The NIO Files.createFile method creates a new and an empty file pointed to by a Path. It will fail with a FileAlreadyExistsException if the file already exists.

Path newFilePath = Paths.get("/Users/JavaDeveloperCentral/data/new-file.txt");
Files.createFile(newFilePath);

The above code will create an empty file called new-file.txt in the Path specified. 

 
There is an overloaded createFile method that accepts a var-args of FileAttribute. One way to create a FileAttribute is to use the PosixFilePermissions class static method asFileAttributes. In the below example, we create a FileAttribute with only Owner write.
FileAttribute<Set<PosixFilePermission>> fileAttributes = PosixFilePermissions
        .asFileAttribute(Set.of(PosixFilePermission.OWNER_WRITE));
Files.createFile(newFilePath, fileAttributes);

Note: Set.of() is a factory method added to the Collection interfaces in Java 9. There is post on Convenience Factory Methods for Collections which talks about all the static factory methods added to the Collections in Java 9.

If you are using Java < 9, then you can use Collections.singleton(PosixFilePermission.OWNER_WRITE) in the above code snippet.

Reading a file #1 – Files.readAllLines

The Files.readAllLines method reads all the lines from a file. This means it loads the entire file’s data into the memory.
Example: Say we have a file called sample-file.txt with the below data.

123 456
abc def
Path filePath = Paths.get("/Users/JavaDeveloperCentral/data/sample-file.txt");
System.out.println(filePath);

Files.readAllLines(filePath)
        .forEach(System.out::println);

This code segments prints the file content as it is,

123 456
abc def

There is another overloaded readAllLines method that accepts a Charset parameter. The above method uses UTF_8 as the default charset. We can pass a Charset as.

Files.readAllLines(filePath, StandardCharsets.UTF_8) //or anything else
        .forEach(System.out::println);

Reading a file #2 – Files.readAllBytes

As the name indicates, this method reads the file as a byte array.

byte[] bytes = Files.readAllBytes(filePath);
System.out.println(new String(bytes));

Reading a file #3 – Files.readString

This method reads the file as a String (simple, isn’t it?). There is an overloaded readString() method that takes the Charset.

Note: This method was added in Java 11.
String data = Files.readString(filePath);
System.out.println(data);

data = Files.readString(filePath, StandardCharsets.UTF_8);
System.out.println(data);

Reading (Streaming a file) – Files.lines

With Java 8, Streams are everywhere. The methods we have seen so far to read the file loads the full data into memory in some format (List of string, byte [] or String). This approach will not work when we want to read a big file. The Java NIO Files class provides a method that allows us to stream the file’s content line by line (each line being a String).

It is the Files.lines method that does this. It reads all the lines from a file identified by a Path as a Stream.  The stream has the reference to the open file. Hence the file will only be closed when the stream is closed. Thus, it is our responsibility to close the stream. Using the Files.lines in a try-with-resource statement is a common way to do it, as it will take care of closing the resource (Stream here).

try (Stream<String> fileStream = Files.lines(filePath)) {
    fileStream.forEach(System.out::println);
}

This will produce the same output as the other read methods seen so far. 

Since we have a stream, we can do all the operations that can be performed on a stream like filtering, mapping, collecting (like toMap and groupingBy)

Example: We can filter only the lines of even length as,

fileStream
        .filter(line -> line.length() %2 == 0)
        .forEach(System.out::println);

Like other read methods, there is a Files.lines() method that accepts the Charset as a second argument.

Writing to a file #1 – Files.writeString

The writeString() method writes a String to a file. It returns the Path of the file written.
Note: This method was added in Java 11.

String data = "some-data";
Path filePath = Paths.get("/Users/JavaDeveloperCentral/data/files/output-file.txt");
Path resultPath = Files.writeString(filePath, data);
System.out.println(resultPath);

The above will write the string some-data to the file named output-file.txt in the path specified. We also print the path to verify the same.

Writing to a file #2 – Files.write

To write a List of Strings, use the write() method. It has two varieties – one which defaults to UTF_8 charset encoding and one which receives the Charset as an argument.

List<String> list = List.of("a", "b", "c");
Files.write(filePath, list);

//Passing a charset
Files.write(filePath, list, StandardCharsets.UTF_8); //or something else

The file written will have the strings one in each line as

a
b
c

Note: If you are using Java < 9, then you can use Arrays.asList(“a”, “b”, “c”) in the above code snippet.

Passing an OpenOption when writing to a file

Both the writeString() and the write() method takes OpenOption. It is a var-args parameter which means we can pass multiple OpenOptions.

OpenOption is a marker interface (it has no methods) and is used to configure how to open or create a file. There are enums which implement the OpenOption interface. I will not cover all the available OpenOptions and will focus on the important ones. 

The StandardOpenOption

A StandardOpenOption is an enum which implements OpenOption. Let us look at a few enum instances from it.

StandardOpenOption.CREATE

  • This will create a new file if the file does not already exist. Since, we can pass multiple open options, this option will be ignored if CREATE_NEW is set.
StandardOpenOption.CREATE_NEW
  • This option will create a new file. It will fail if the file already exists.
StandardOpenOption.WRITE
  • As the name suggests, this mode is used to write data.
StandardOpenOption.APPEND
  • After we have opened the file for writing, this enum tells to write the data to the end of the file (append to the file). The existing data will be preserved.
StandardOpenOption.TRUNCATE_EXISTING
  • Unlike APPEND, this option will set the length of the file to 0, thus truncating or clearing out all the existing data.

Let us use these OpenOptions.. 

OpenOption – Opening a file for writing

Files.writeString(filePath, data, StandardOpenOption.WRITE);

The above will open a file for writing. It is necessary for the file to exist. If the file does not exist, it will throw a NoSuchFileException.

Use CREATE_NEW to create a new file and write the data.

Files.writeString(filePath, data, StandardOpenOption.CREATE_NEW);

Note that if the file exists, it will throw FileAlreadyExistsException.

If you want to create a file if it does not exist but want to write to the file if it already exists, use CREATE.
Files.writeString(filePath, data, StandardOpenOption.CREATE);

Summary:

  • If you are sure that the file exists, use WRITE.
  • If you are sure that the file does not exist when writing, use CREATE_NEW.
  • Finally, if you do not care if the file already exists or not, use CREATE which will create a new file if it does not exist.

OpenOption – Writing

Now that we have opened a file successfully for writing, we can specify how to write the data. Using APPEND, we write to the end of the file. But by using TRUNCATE_EXSITING we clear the current file data (if not empty) and start writing from the beginning.

Putting it all together, here are some combinations of the OptionOptions.

//File exists - Append to it
Files.writeString(filePath, data, StandardOpenOption.WRITE, StandardOpenOption.APPEND);

//File exists - Truncate existing data
Files.writeString(filePath, data, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);

//File does not exist, create a new one and write
Files.writeString(filePath, data, StandardOpenOption.CREATE_NEW);

//Create file if it does not exist and Append to it
Files.writeString(filePath, data, StandardOpenOption.CREATE, StandardOpenOption.APPEND);

//Create file if it does not exist - Truncate existing data
Files.writeString(filePath, data, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);

In our initial write examples, we did not pass any OpenOptions. In that case, it would behave as if StandardOpenOption.CREATEStandardOpenOption.TRUNCATE_EXSITING and StandardOpenOption.WRITE options were passed. In other words, it will open the file for writing if it exists, else it will create a new file. It will truncate the file contents if it was not empty.

Specifying only WRITE/CREATE OpenOption

Be careful if you specify only CREATE (when the file already exists) or when you specify only WRITE. Since we haven’t passed neither APPEND and nor TRUNCATE_EXISTING, it will start writing from the beginning. This will corrupt the data in the file (and presumably which is not what you intend for).

Example: Say the file has some-data as the content and,

Files.writeString(filePath, data, StandardOpenOption.WRITE);

The resultant file content will be test-data.

Copying a file with NIO Files

Let us look at what APIs Java NIO Files has for copying files. A copy operation has a source and a destination. In addition to this, the copy method takes a CopyOption. A CopyOption is a marker interface (like a OpenOption). This object configures how to copy or move a file.

The StandardCopyOption

StandardCopyOption is one of the enums that implement the CopyOption.

StandardCopyOption.REPLACE_EXISTING

  • If this is set, it will replace the destination file if it already exists.
StandardCopyOption.COPY_ATTRIBUTES:
  • This copies the file attributes of the source to the destination file. The exact file attributes that are copied is platform and file system dependent and hence not specified. An example of file attributes are creation time, last modified time, etc.
StandardCopyOption.ATOMIC_MOVE
  • Moves the file as an atomic file system operation. The copy method does not support this.

By default, if the destination exists, the copy method will result in a FileAlreadyExistsException.

Path sourceFilePath = Paths.get("/Users/JavaDeveloperCentral/data/files/source.txt");
Path destinationFilePath = Paths.get("/Users/JavaDeveloperCentral/data/files/destination.txt");

Files.copy(sourceFilePath, destinationFilePath, StandardCopyOption.REPLACE_EXISTING);

The above call to the NIO Files.copy will copy the contents of the file in source.txt to the file destination.txt. If the file destination.txt exists, it will be replaced.

Copying a file – Other variations

Other than specifying the source and destinations as a Path, we can use an InputStream for a source or an OutputStream for the destination.

Let us create a BufferedInputStream from the source file and pass it to Files.copy. The result will be the same – source file data will be copied into the destination file.

InputStream is = new BufferedInputStream(new FileInputStream(sourceFilePath.toFile()));
Files.copy(is, destinationFilePath, StandardCopyOption.REPLACE_EXISTING);

Using an OutputStream as the target destination looks like.

OutputStream os = new BufferedOutputStream(new FileOutputStream(destinationFilePath.toFile()));
Files.copy(sourceFilePath, os);//no CopyOption when using a OutputStream
os.flush();//have to close/flush when using a BufferedOutputStream

Note that the outputStream has to be closed/flushed to have the data written properly. Alternatively, we can use a try-with-resources to automatically close the output stream.

try( OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(destinationFilePath.toFile()))) {
    Files.copy(sourceFilePath, outputStream);
}

Moving a file

We use the Files.move method to move or rename a file. It fails with a FileAlreadyExistsException, if the target already exists. If we wish to overwrite the target if it exists, we can use the REPLACE_EXISTING as the CopyOption.

Path sourceFilePath = Paths.get("/Users/JavaDeveloperCentral/data/files/source.txt");
Path destinationFilePath = Paths.get("/Users/JavaDeveloperCentral/data/files/new_source.txt");

Files.move(sourceFilePath, destinationFilePath, StandardCopyOption.REPLACE_EXISTING);

The NIO Files.move method supports ATOMIC_MOVE CopyOption. But, the implementation can throw an AtomicMoveNotSupportedException if the move cannot be done atomically.

Deleting a file

This is a simple method that deletes the file pointed to by the path passed.

Path filePathToDelete = Paths.get("/Users/JavaDeveloperCentral/data/files/file.txt");
Files.delete(filePathToDelete);

If the file does not exist, it throws a NoSuchFileException. 

 
We can use the deleteIfExists method to delete a file only if it exists.
Files.delete(filePathToDelete); //deletes only if it exists

Checking if a file exists

The File.exits methods returns a boolean if the file exits.

Path sourceFilePath = Paths.get("/Users/JavaDeveloperCentral/data/files/source.txt");
System.out.println(Files.exists(sourceFilePath));

Apart from the Path parameter, it accepts a var-args parameter of type LinkOption. A LinkOption is an enum with one enum instance NOFOLLOW_LINKS. It says not to follow symbolic links. We can pass this if we want to follow symbolic links when checking if a file exists. By default, symbolic links are followed.

Path sourceFilePath = Paths.get("/Users/JavaDeveloperCentral/data/files/source.txt");
System.out.println(Files.exists(sourceFilePath, LinkOption.NOFOLLOW_LINKS));

Conclusion

We covered the common methods from the Java NIO Files class for operating on a file. First, we looked at the Path class since all the static methods in the Files class operate using a Path instance to point to a file. Then we looked at the most common operations performed on a file viz., creating, reading, writing, copying, moving, and deleting a file. 
I hope this would have been a detailed explanation of the NIO Files methods. I would like to hear your comments on this.

References

Leave a Reply