Working with files in Java in the post Java 7 Era
Abstract:
Java has been here for more than 19 years and it has evolved so much from the initial version. Some of these changes were accepted widely, while few changes haven't caught much of attention from the mainstream developer community. I believe file API is one such under explored area. Java 7 introduced second generation of 'nio' APIs, which provides an improved way of working with Files. This article will show examples from pre Java 7 world and demonstrate you how to write them if you were to write that code today.
The pre Java 7 world
The following code shows the typical way of accessing files in Java. Assume the below code is inside the main
method.
final String rootLocation = "/Users/naresha/work";
File root = new File(rootLocation);
System.out.println(root); // prints "/Users/naresha/work"
As show in the example, access to the file systems are acheieved though java.io.File
class. Let's get more information about the work
directory from the File
object we assigned to the variable root
.
System.out.println("Exists: " + root.exists()); // true
System.out.println("Is Directory: " + root.isDirectory()); // true
What's wrong with this?
Here are some of the limitations with these APIs.
- Getting the abstraction right is not always easy. Few file operations did not work consistently across platforms.
- Few methods failed silently, without providing the reason for failure (throwing appropriate exceptions)
- Lack of support for symbolic links
As part of Java 7 new nio APIs, alternate file system APIs were developed, which would oversome the above limitations.
File system abstraction
The abstract class java.nio.file.FileSystem
provides a nice abstraction of file systems. Below is a list of FileSystem implementations for popular platforms.
- sun.nio.fs.LinuxFileSystem
- sun.nio.fs.MacOSXFileSystem
- sun.io.fs.WindowsFileSystem
- sun.io.fs.SolarisFileSystem
The programmers don't even have to know about the existance of multiple implementations most of the time! You just have to program against FileSystem
. The following code snippet shows how to get your file system.
FileSystem defaultFileSystem = FileSystems.getDefault();
System.out.println("Default FileSystem: " + defaultFileSystem);
// Default FileSystem: sun.nio.fs.MacOSXFileSystem@78308db1
final String rootLocation = "/Users/naresha/work";
Path root = defaultFileSystem.getPath(rootLocation);
System.out.println("Path: " + root);
// Path: /Users/naresha/work
Once you have a reference to a FileSystem
object, you can access any path within that file system by invoking getPath
method. This returns an object of type java.nio.file.Path
. (Path is an interface)
Does it sound complicated? Don't worry. Java provides java.nio.file.Paths
class, whose static methods encapsulate the FileSystem API.
Path root = Paths.get(rootLocation);
System.out.println("Path: " + root);
// Path: /Users/naresha/work
Instead of passing the path as a single string, you could pass them as multiple strings as well.
Path r2 = Paths.get("/", "Users", "naresha", "work");
System.out.println("Path: " + r2);
// Path: /Users/naresha/work
Getting information about files
If you have alreay browsed though the methods of Paths
interface, you might have been surprised to find that there no methods to check if the path exists; or if the path is a directory(which were present in java.io.File
as we used earlier). In nio 2, those methods are present as static methods under java.nio.file.Files
.
System.out.println("Exists: " + Files.exists(root));
// Exists: true
System.out.println("Diretory? " + Files.isDirectory(root));
// Diretory? true
Creating files
Here is the pre Java 7 version
File toc = new File(root, "toc.text");
try{
boolean created = toc.createNewFile();
System.out.println("Created? " + created);
}catch(IOException ex){
System.out.println(ex);
}
The boolean method createNewFile
returns true, if it could create the file successfully. But if it returns false, you don't have any details on why it could not create the file. Now let's take a look at the nio 2 version.
Path toc = root.resolve("toc.txt");
try{
Path createdFile = Files.createFile(toc);
System.out.println("Created " + createdFile);
// Created /Users/naresha/work/toc.txt
}catch(Exception ex){
System.out.println(ex);
}
Since we already have a Path
object for "/Users/naresha/work", we invoke resolve
method on root to create a Path
object relative to the path denoted by root. Note that Files.ceateFile
returns Path
to the created file. If you try to run the program for the second time, you would get the following exception.java.nio.file.FileAlreadyExistsException: /Users/naresha/work/toc.txt
. The method could also throw one of java.lang.SecurityException
, java.io.IOException
and java.lang.UnsupportedOperationException
.
Deleting files
Lets try to delete a non existing file
boolean deleteStatus = new File(root, "temp").delete();
System.out.println("Deleted? " + deleteStatus);
// Deleted? false
The delete
method on File returns boolean value indicating the status. Also the only exception that could possibly be thrown is java.lang.SecurityException
. Here we dont get the reason why the file was not deleted. Let's look at the nio 2 delete
method.
Path nonExistingDir = Paths.get(rootLocation, "temp");
try {
Files.delete(nonExistingDir);
}catch(IOException ex){
System.out.println(ex);
}
// java.nio.file.NoSuchFileException: /Users/naresha/work/temp
The nio 2 delete
method is void and the exception thrown gives further details on why it could not delete the file. If you attempt to delete a non-empty directory, you would get a java.nio.file.DirectoryNotEmptyException
. Additionally delete
method can throw java.lang.SecurityExcepion
or java.io.IOException
.
If you anticipate that the directory might not exist, you could use deleteIfExists
method, which returns a boolean value indicating if the file was deleted.
Creating directories
Following were the methods used to create directories prior to Java 7 - from java.io.File
- mkdir - creates directory as mentioned by the File object.
- mkdirs - create directories, including any non existant parent directories.
Corresponsing nio 2 static methods are present in java.nio.file.Files
- createDirectory
- createDirectories
Copy files
If you have worked with Java versions prior to 7, you could remember writing a file copy utility method in every project you worked on. With nio 2, you could copy files as follows
try {
Files.copy(Paths.get(rootLocation, "groovy", "Hello.groovy"),
Paths.get(rootLocation, "groovy", "HelloWorld.groovy"),
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
}catch (IOException ex){
System.out.println(ex);
}
Move files
Support we want to move the file readme.txt from "/Users/naresha/work/groovy" to "/Users/naresha/work/docs"
Path source = Paths.get(rootLocation, "groovy", "readme.txt");
Path target = Paths.get(rootLocation, "docs");
try {
Files.move(source, target.resolve(source.getFileName()), StandardCopyOption.REPLACE_EXISTING);
}catch(IOException ex){
System.out.println(ex);
}
Interoperablity
At this point you might think that a library you are using is preventing you from using nio 2 APIs, because it accepts or returns java.io.File
. In such cases, toFile
and toPath
methods are handy.
// File to Path (root is of type File)
Path path = root.toPath();
// Path to File (source is of type Path)
File file = source.toFile();
Working with symbolic links
Prior to nio 2, Java did not have any APIs to work with symbolic links.
The below example shows how to create a symbolic link.
Path javaHome = Paths.get("/Library/Java/JavaVirtualMachines/jdk1.8.0_05.jdk/Contents/Home");
Path link = Paths.get(rootLocation, "jdk");
Path jvmLink;
try{
jvmLink = Files.createSymbolicLink(link, javaHome);
System.out.println("Sym link: " + jvmLink);
}catch(IOException ex){
System.out.println(ex);
}
You should find the symbolic link created as shown below
[work ]$ tree -L 1
.
├── docs
├── groovy
├── java
├── jdk -> /Library/Java/JavaVirtualMachines/jdk1.8.0_05.jdk/Contents/Home
└── toc.txt
You could check if a Path
object denotes a symbolic link as follows.
System.out.println("jdk is symlink? " + Files.isSymbolicLink(link)); // true
System.out.println("javaHome is symlink? " + Files.isSymbolicLink(javaHome)); // false
Reading and Writing Files
Usually you would use an object of java.io.BufferedReader
to read the contents of a text file. The following example shows how to create a BufferedReader
object prior to nio 2.
File toc = new File(root, "toc.text");
try {
BufferedReader reader = new BufferedReader(new FileReader(toc));
// use reader & close
} catch (FileNotFoundException e) {
e.printStackTrace();
}
Below is the nio 2 version
Path toc = Paths.get(rootLocation, "toc.txt");
try(BufferedReader reader = Files.newBufferedReader(toc, StandardCharsets.UTF_8)) {
System.out.println("Reader: " + reader);
// use reader
} catch (IOException e) {
e.printStackTrace();
}
Similarly you could create a BufferedWriter
object from a Path
object, using Files.newBufferedWriter
method.
Improvements in Java 8
Java 8 further improves the nio 2 API.
The following example shows how to read a text file using readAllLines
method.
public static void readText() throws Exception{
Path readmeFile = Paths.get("/Users/naresha/work/docs", "readme.txt");
List<String> lines = Files.readAllLines(readmeFile);
lines.forEach(System.out::println);
}
Suppose you have a large file and you want to get the first match for a word in the beginning of the line. In this case you would use lines
method which would read the lines lazily, making your program efficient.
public static void readTextLazy() throws Exception{
Path largeFile = Paths.get("/Users/naresha/work/docs", "largefile.txt");
String firstMatch = Files.lines(largeFile)
.filter((line) -> line.startsWith("Four"))
.findFirst()
.get();
System.out.println("First Match: " + firstMatch);
}
Conclusion
The lessons learned from using java File APIs for years are applied while creating the new nio file APIs. As we have seen in the above examples, they provide wider functionalities, provide more control to the programmers, provide better ways to express code and better performance. I am not claiming that all of us should change our existing code to use these new APIs. While older code will continue to be in use, there shouldn't be any excuse for not using them in the code you write today(unless you are stuck with Java 6!).
References
Java 8 API Documentation
[Originally published in September 2014]