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
:
Value | Description |
---|---|
APPEND | If the file is opened for WRITE access then bytes will be written to the end of the file rather than the beginning. |
CREATE | Create a new file if it does not exist. |
CREATE_NEW | Create a new file, failing if the file already exists. |
DELETE_ON_CLOSE | Delete on close. |
DSYNC | Requires that every update to the file’s content be written synchronously to the underlying storage device. |
READ | Open for read access. |
SPARSE | Sparse file. |
SYNC | Requires that every update to the file’s content or metadata be written synchronously to the underlying storage device. |
TRUNCATE_EXISTING | If the file already exists and it is opened for WRITE access, then its length is truncated to 0. |
WRITE | 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 CREATE
, TRUNCATE_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 StandardOpenOption
s 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 StandardOpenOption
s to customize streams behavior. By default, when no StandardOpenOption
is passed on to the method, streams write
method behaves as if the CREATE
, TRUNCATE_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.
Value | Description |
---|---|
ATOMIC_MOVE | Move the file as an atomic file system operation. |
COPY_ATTRIBUTES | Copy attributes to the new file. |
REPLACE_EXISTING | 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 classFiles
) - 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 classFiles
) - 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. |