Core Java

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
ValueDescription
APPENDIf the file is opened for WRITE access then bytes will be written to the end of the file rather than the beginning.
CREATECreate a new file if it does not exist.
CREATE_NEWCreate a new file, failing if the file already exists.
DELETE_ON_CLOSEDelete on close.
DSYNCRequires that every update to the file’s content be written synchronously to the underlying storage device.
READOpen for read access.
SPARSESparse file.
SYNCRequires that every update to the file’s content or metadata be written synchronously to the underlying storage device.
TRUNCATE_EXISTINGIf the file already exists and it is opened for WRITE access, then its length is truncated to 0.
WRITEOpen 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
ValueDescription
ATOMIC_MOVEMove the file as an atomic file system operation.
COPY_ATTRIBUTESCopy attributes to the new file.
REPLACE_EXISTINGReplace 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
Reference: Working with files and directories in NIO.2 from our JCG partner Jakub Stas at the Jakub Stas blog.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button