Working with files and directories in NIO.2

In previous articles I discussed creation (Creating files and directories) and selection (Listing and filtering directory contents) of files and directories. The last logical step to take is to explore what can we do with them and how. This is a part of the library that was redesigned in a big way. Updates in this area include guarantee of atomicity of certain operations, API improvements, performance optimization as well as introduction of proper exception hierarchy that replaced boolean returning methods from prior versions of IO library.

Opening a file

Before we get down to reading from and writing to a file, we need to cover one common ground of these operations – the way files are opened. The way files are opened directly influences results of these operations as well as their performance. Lets take a look at standard options of opening files contained in enum java.nio.file.StandardOpenOption:

Standard open options
Value Description
MARKDOWN_HASH375ffb668aa90f1c7fcae55e9734a752MARKDOWN_HASH If the file is opened for WRITE access then bytes will be written to the end of the file rather than the beginning.
MARKDOWN_HASH294ce20cdefa29be3be0735cb62e715dMARKDOWN_HASH Create a new file if it does not exist.
MARKDOWN_HASHc525d9d1cc7e7b50a14627290422a1a6MARKDOWN_HASH Create a new file, failing if the file already exists.
MARKDOWN_HASHf3ff5294782ff6a18cb2b05509d3da71MARKDOWN_HASH Delete on close.
MARKDOWN_HASH1d43ef7311cf96b18fb41da3f5b0c67cMARKDOWN_HASH Requires that every update to the file's content be written synchronously to the underlying storage device.
MARKDOWN_HASH3466fab4975481651940ed328aa990e4MARKDOWN_HASH Open for read access.
MARKDOWN_HASH0459833ba9cad7cfd7bbfe10d7bbbe6eMARKDOWN_HASH Sparse file.
MARKDOWN_HASH274ccef15a21e829d03293a6fd1974f3MARKDOWN_HASH Requires that every update to the file's content or metadata be written synchronously to the underlying storage device.
MARKDOWN_HASH1e7e3a6cdb72739667556ac016c90f47MARKDOWN_HASH If the file already exists and it is opened for WRITE access, then its length is truncated to 0.
MARKDOWN_HASHd4b9e47f65b6e79b010582f15785867eMARKDOWN_HASH Open for write access.

These are all standard options that you as a developer may need to properly handle opening of files whether it is for reading or writing.

Reading a file

When it comes to reading files NIO.2 provides several ways to do it – each with its pros and cons. These approaches are as follows:

  • Reading a file into a byte array
  • Using unbuffered streams
  • Using buffered streams

Lets take a look at first option. Class Files provides method readAllBytes to do exactly that. Reading a file into a byte array seems like a pretty straight forward action but this might be suitable only for a very restricted range of files. Since we are putting the entire file into the memory we must mind the size of that file. Using this method is reasonable only when we are trying to read small files and it can be done instantly. It is pretty simple operation as presented in this code snippet:

Path filePath = Paths.get("C:", "a.txt");

if (Files.exists(filePath)) {
    try {
        byte[] bytes = Files.readAllBytes(filePath);
        String text = new String(bytes, StandardCharsets.UTF_8);

        System.out.println(text);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

The code above first reads a file into a byte array and then constructs string object containing contents of said file with following output:

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sit amet justo nec leo euismod porttitor. Vestibulum id sagittis nulla, eu posuere sem. Cras commodo, massa sed semper elementum, ligula orci malesuada tortor, sed iaculis ligula ligula et ipsum.

When we need to read the contents of a file in string form we can use the code above. However, this solution is not that clean and we can use readAllLines from class Files to avoid this awkward construction. This method serves as a convenient solution to reading files when we need human-readable output line by line. The use of this method is once again pretty simple and quite similar to the previous example (same restrictions apply):

Path filePath = Paths.get("C:", "b.txt");

if (Files.exists(filePath)) {
    try {
        List<String> lines = Files.readAllLines(filePath, StandardCharsets.UTF_8);

        for (String line : lines) {
            System.out.println(line);
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

With following output:

Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aliquam sit amet justo nec leo euismod porttitor.
Vestibulum id sagittis nulla, eu posuere sem.
Cras commodo, massa sed semper elementum, ligula orci malesuada tortor, sed iaculis ligula ligula et ipsum.

Reading a file using streams

Moving on to more sophisticated approaches we can always use good old streams just like we were used to from prior versions of the library. Since this is a well-known ground I’m only going to show how to get instances of these streams. First of all, we can retrieve InputStream instance from class Files by calling newInputStream method. As usual, one can further play with a decorator pattern and make a buffered stream out of it. Or for a convenience use method newBufferedReader. Both methods return a stream instance that is plain old java.io object.

Path filePath1 = Paths.get("C:", "a.txt");
Path filePath2 = Paths.get("C:", "b.txt");

InputStream is = Files.newInputStream(filePath1);
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);

BufferedReader reader = Files.newBufferedReader(filePath2, StandardCharsets.UTF_8);

Writing to a file

Writing to a file is similar to reading process in a range of tools provided by NIO.2 library so lets just review:

  • Writing a byte array into a file
  • Using unbuffered streams
  • Using buffered streams

Once again lets explore the byte array option first. Not surprisingly, class Files has our backs with two variants of method write. Either we are writing bytes from an array or lines of text, we need to focus on StandardOpenOptions here because both methods can be influenced by custom selection of these modifiers. By default, when no StandardOpenOption is passed on to the method, write method behaves as if the CREATETRUNCATE_EXISTING, and WRITE options were present (as stated in Javadoc). Having said this please beware of using default (no open options) version of write method since it either creates a new file or initially truncates an existing file to a zero size. File is automatically closed when writing is finished – both after a successful write and an exception being thrown. When it comes to file sizes, same restrictions as in readAllBytes apply.

Following example shows how to write an byte array into a file. Please note the absence of any checking method due to the default behavior of write method. This example can be run multiple times with two different results. First run creates a file, opens it for writing and writes the bytes from the array bytes to this file. Any subsequent calling of this code will erase the file and write the contents of the bytes array to this empty file. Both runs will result in closed file with text ‘Hello world!’ written on the first line.

Path newFilePath = Paths.get("/home/jstas/a.txt");
byte[] bytes = new byte[] {0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21};

try {
    Files.write(newFilePath, bytes);
} catch(IOException e) {
    throw new RuntimeException(e);
}

When we need to write lines instead of bytes we can convert a string to byte array, however, there is also more convenient way to do it. Just prepare a list of lines and pass it on to write method. Please note the use of two StandardOpenOptions in following example. By using these to options I am sure to have a file present (if it does not exist it gets created) and a way to append data to this file (thus not loosing any previously written data). Whole example is rather simple, take a look:

Path filePath = Paths.get("/home/jstas/b.txt");

List<String> lines = new ArrayList<>();
lines.add("Lorem ipsum dolor sit amet, consectetur adipiscing elit.");
lines.add("Aliquam sit amet justo nec leo euismod porttitor.");
lines.add("Vestibulum id sagittis nulla, eu posuere sem.");
lines.add("Cras commodo, massa sed semper elementum, ligula orci malesuada tortor, sed iaculis ligula ligula et ipsum.");

try {
    Files.write(filePath, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException e) {
    throw new RuntimeException(e);
}

Writing to a file using streams

It might not be a good idea to work with byte arrays when it comes to a larger files. This is when the streams come in. Similar to reading chapter, I’m not going to explain streams or how to use them. I would rather focus on a way to retrieve their instances. Class Files provides method newOutputStream that accepts StandardOpenOptions to customize streams behavior. By default, when no StandardOpenOption is passed on to the method, streams write method behaves as if the CREATETRUNCATE_EXISTING, and WRITE options are present (as stated in Javadoc). This stream is not buffered but with a little bit of decorator magic you can create BufferedWriter instance. To counter this inconvenience, NIO.2 comes with newBufferWriter method that creates buffered stream instance right away. Both ways are shown in following code snippet:

Path filePath1 = Paths.get("/home/jstas/c.txt");
Path filePath2 = Paths.get("/home/jstas/d.txt");

OutputStream os = Files.newOutputStream(filePath1);
OutputStreamWriter osw = new OutputStreamWriter(os);
BufferedWriter bw = new BufferedWriter(osw);

BufferedWriter writer = Files.newBufferedWriter(filePath2, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);

Copying and moving files and directories

Copying files and directories

One of most welcomed features of NIO.2 is updated way of handling copying and moving files and directories. To keep everything nicely in line, designers decided to introduce two parent (marker) interfaces into new file system API: OpenOption and CopyOption (both interfaces from package java.nio.file). StandardOpenOption enum mentioned in previous chapter implements OpenOption interface. CopyOption interface on the other hand has two implementations, one of which we have already met in post about Links in NIO.2. Some of you may recall LinkOption enum which is said implementation guiding methods handling link related operations. However, there is another implementation – StandardCopyOption enum from package java.nio.file. Once again, we are presented with yet another enumeration – used to guide copy operations. So before we get down to any code lets review what we can achieve using different options for copying.

Standard copy options
Value Description
MARKDOWN_HASHbf8ed7dfa39dc0c9951569451ebf4f75MARKDOWN_HASH Move the file as an atomic file system operation.
MARKDOWN_HASH9450dfddf13fffe8e7ab5f08b052a95eMARKDOWN_HASH Copy attributes to the new file.
MARKDOWN_HASH129e13fde94a87725f64aab0e228c309MARKDOWN_HASH Replace an existing file if it exists.

Using these options to guide your IO operations is quite elegant and also simple. Since we are trying to copy a file, ATOMIC_MOVE does not make much sense to use (you can still use it, but you will end up with java.lang.UnsupportedOperationException: Unsupported copy option). Class Files provides 3 variants of copy method to serve different purposes:

  • copy(InputStream in, Path target, CopyOption... options)
    • Copies all bytes from an input stream to a file.
  • copy(Path source, OutputStream out)
    • Copies all bytes from a file to an output stream.
  • copy(Path source, Path target, CopyOption... options)
    • Copy a file to a target file.

Before we get to any code I believe that it is good to understand most important behavioral features of copy method (last variant out of three above). copy method behaves as follows (based on Javadoc):

  • By default, the copy fails if the target file already exists or is a symbolic link.
  • If the source and target are the same file the method completes without copying the file. (for further information check out method isSameFile of class Files)
  • File attributes are not required to be copied to the target file.
  • If the source file is a directory then it creates an empty directory in the target location (entries in the directory are not copied).
  • Copying a file is not an atomic operation.
  • Custom implementations may bring new specific options.

These were core principals of inner workings of copy method. Now is a good time to look at code sample. Since its pretty easy to use this method lets see it in action (using the most common form of copy method). As expected, following code copies the source file (and possibly overwrites the target file) preserving file attributes:

Path source = Paths.get("/home/jstas/a.txt");
Path target = Paths.get("/home/jstas/A/a.txt");

try {
    Files.copy(source, target, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
    throw new RuntimeException(e);
}

No big surprises here – code copies source file with its file attributes. If you feel I forgot about (not empty) directories, let me assure you that I did not. It is also possible to use NIO.2 to copy, move or delete populated directories but this is what I am going to cover in the next post so you gonna have to wait a couple of days.

Moving files and directories

When it comes to moving files we again need to be able to specify options guiding the process for the method move from Files class. Here we make use of StandardCopyOptions mentioned in previous chapter. Two relevant options are ATOMIC_MOVE and REPLACE_EXISTING. First of all, lets start with some basic characteristics and then move on to a code sample:

  • By default, the move method fails if the target file already exists.
  • If the source and target are the same file the method completes without moving the file. (for further information check out method isSameFile of class Files)
  • If the source is symbolic link, then the link itself is moved.
  • If the source file is a directory than it has to be empty to be moved.
  • File attributes are not required to be moved.
  • Moving a file can be configured to be an atomic operation but doesn’t have to.
  • Custom implementations may bring new specific options.

Code is pretty simple so lets look at following code snippet:

Path source = Paths.get("/home/jstas/b.txt");
Path target = Paths.get("/home/jstas/A/b.txt");

try {
    Files.move(source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch(IOException e) {
    throw new RuntimeException(e);
}

As expected, code moves source file in an atomic operation.

Removing files and directories

Last part of this article is dedicated to deleting files and directories. Removing files is, once again, pretty straight forward with two possible methods to call (both from Files class, as usual):

  • public static void delete(Path path)
  • public static boolean deleteIfExists(Path path)

Same rules govern both methods:

  • By default, the delete method fails with DirectoryNotEmptyException when the file is a directory and it is not empty.
  • If the file is a symbolic link then the link itself is deleted.
  • Deleting a file may not be an atomic operation.
  • Files might not be deleted if they are open or in use by JVM or other software.
  • Custom implementations may bring new specific options.
Path newFile = Paths.get("/home/jstas/c.txt");
Path nonExistingFile = Paths.get("/home/jstas/d.txt");

try {
    Files.createFile(newFile);
    Files.delete(newFile);

    System.out.println("Any file deleted: " + Files.deleteIfExists(nonExistingFile));
} catch(IOException e) {
    throw new RuntimeException(e);
}

With an output:

Any file deleted: false

Leave a Reply

Your email address will not be published. Required fields are marked *