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();

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]

Show Comments