In old versions, you could use the java.io.File to handle files. But there were problems with symbolic link, attributes, performance. The Java 1.4 introduced the NIO API, but it was not enough to solve the problems. The version 7 brings the NIO.2 API such a solution to help in the support to manipulate files and directories. Instead to use java.io.File, now you can use java.nio.file package (Path, Paths, and Files).

It is the sixth post of the Java 8 series about the changes that you can find from version 6 to 8 and it will introduce an overview about the NIO.2 API.

Summary


Manipulating files and directories

Java NIO.2 API is a replacement for the java.io.File class. This API will make our work with files easier.

  • java.nio.file.Path interface: "A Path object represents a hierarchical path on the storage system to a file or directory. Path is a direct replacement for the legacy java.io.File class"
  • java.nio.file.Paths: It is a factory class to create instance of Path, which is a location within the file system, and not the actual file.
  • Helper class: It also has static methods but it differs in Paths because helper classes are focused on manipulating or creating new objects from existing instances, whereas factory classes are focused primarily on object creation. Many of the same operations available in java.io.File are available to java.nio.file.Path via a helper class called java.nio.file.Files.
# Create
String str = "/home/a/b/c/d.txt";
Path p1 = Paths.getPath(str)
Path p2 = FileSystems.getDefault().getPath(str);
Path p3 = FileSystems.getFileSystem(
    new URI("http://ex.com")).getPath(String);
Path p4 = Paths.getPath(new URI("file:///home/directory"))
Path psub = p1.subpath(1,3) // a/b

# Join
Path p5 = Paths.getPath("/home/a");
Path p6 = Paths.getPath("b");
Path p7 = p5.resolve(p6); // /home/a/b

# Create Directory
Files.createDirectory(p7);
# Working with Legacy (new method - toPath)
File file = new File("/home/img.png");
Path pFile = file.toPath();

# Use
Paths.get(new URI("http://www.example.com")).toUri();
Paths.get("/home/../test").getParent().normalize().toAbsolutePath();

# View - Methods
pFile.toString()     // /home/img.png
pFile.getNameCount() // 2
getName(i)           // 0: home; 1: img.png

# Access - Methods
pFile.getFileName(); // img.png
pFile.getParent();   // /home
pFile.getRoot();     // /

# Check - Methods
pFile.isAbsolute()    // true
pFile.toAbsolutePath()// /home/img.png
Paths.getPath(".././d.txt").toRealPath(); // /home/a/b/c/d.txt

# Helper Class
Files.exists(pFile);
Files.isSameFale(p1, pFile);

# Copy - file or directory
Files.copy(p1,p7);
Files.copy(new FileInputStream("test.txt"), p1);
Files.copy(p1, new FileOutputStream("test.txt"));

# move
Files.move(pFile, Paths.get("/home/image.png")); // rename
Files.move(pFile, Paths.get("/home/a/image.png")); // move

# Remove
Files.delete(p1); // throw an exception is file not exist
Files.deleteIfExists(p1); // return false if file not exist

# read and write
BufferedReader reader = Files.newBufferedReader
                  (p1,Charset.forName("US-ASCII"))
List lines = Files.readAllLines(p1)
BufferedWrite writer = Files.newBufferedWriter
                  (path,Charset.forName("UTF-16"))

File Attributes

File attributes are related to the metadata (not the contents of the file), such be hidden or about permission. You can reading common attributes using some Files methods such isDerectory, isRegularFile, isSymbolicLink. isHidden, isReadable, isExecutable, size. Also it's possible managing the modifications (getLastModifiedTime, setLastModifiedTime) and  ownership (getOwner, setOwner.).

However, there are costs to access the file and get the metada, and some attributes are file system specific. The NIO.2 API allow you to construct views for several systems in one call. The view is a group of attributes related to a file system. If you need access many attributes, the use of view can help the performance.

All attributes classes extend from BasicFileAttributes, so you can use the common methods.

# Access view information
//Files.readAttibutes(path, Class)
BasicFileAttributes data =
   Files.readAttibutes(p1,BasicFileAttributes.class)
data.isDirectory();
data.isRegularFile();
data.isSymbolicLink();
data.size();
data.lastModifiedTime();

//Files.getFileAttibuteView(path, Class)
BasicFileAttributeView view =
   Files.getFileAttributeView(p1,BasicFileAttributeView.class);
view.setTimes(FileTime.fromMillis(10_000), null, null);

Walking by the structured path

To find a file or directory, you should walk by the path that is structured as a tree. So, you will start with the parent directory and iterate over its descendants.

The strategies to walk in the directory tree are depth-first search and breadth-first search.

  • depth-first search: from root to an leaf and then back up toward the root. The search depth is the distance from root to current node.
  • breadth-first search: start at the root node and process all elements of each particular depth before go to the nest level.

The Streams API use the depth-first search strategy to traverse a directory. It happens in a lazy manner, which means that the elements are built and read while the directory is being traversed.

# Walk
Stream streamPath = Files.walk("/home");
streamPath.filter(p->p.toString().endsWith(".txt"))
      .forEach(System.out::println);

//PS: walk method follows symbolic links if FileVisitOption.FOLLOW_LINKS is passad as an argument
//PS: walk method traverses subdirectories recursively

# Find
path path = Paths.get("/home");
long dateFilter = 142007040000L;
int depthLimit = 10

Stream stream = Files.find(path, depthLimit,
    (p,a)-> p.toString().endsWith(".txt")
         && a.lastModifiedTime().toMillis() > dateFilter);
stream.forEach(System.out::println);

//PS: find method follows symbolic links if FileVisitOption.FOLLOW_LINKS is passad as an argument

# Find with PathMatcher
// Ex pattern: '*.java', '*.*','*[0-9]*','*.{java,class}','/home/*/*'
PathMatcher pm = FileSystems.getDefault()
     .getPathMatcher("glob:"+pattern);
boolean isMatch = pm.matches(path.getName());
# Line: replace the old readAllLines(), it results OutOfMemoruError
Path path = Paths.get("/home/a/test.txt");
Files.lines(path).forEach(System.out::println);
// If file does not exists throws an IOException

PS: more about getPathMatcher you can see here
https://docs.oracle.com/javase/8/docs/api/java/nio/file/FileSystem.html#getPathMatcher-java.lang.String-

To manager circular Path, the NIO.2 API offers the option to inform FOLLOW_LINKS to the walk method and then specify a depth limit. It will track the path visited and throw an exception if a cycle is detected.

Recursively access a directory tree

The DirectoryStream interface  (which does not implement or extend the Stream interface) is used to read a single directory. It can be used to iterate over a directory, such as the listFiles method.

Path path = Path.get("/home/a");
try (DirectoryStream str = Files.newDirectoryStream(path)) {
    for(Path p: str){
      System.out.println(p.getFileName()+":"+Files.isDirectory(p));
    }
}

The FileVisitor interface is used to visit an entire directory tree. To use this you need implemet the interface and then pass a Files.walkFileTree() method that will handle process to load the next directory and file. If you use Path such the generic type you need four methods: visitFile, visitFileFailed, preVisitDirectory and postVisitDirectory.

public class A implements FileVisitor{

    public FileVisitResult preVisitDirectory(Path dir,
            BasicFileAttributes a)thrws IOException{
      return FileVisitResult.CONTINUE;
   }

   public FileVisitResult visitFile(Path file,
           BasicFileAttributes a)thrws IOException{
      if(/*CONDITION*/){FileVisitResult.TERMINATE;}
      else {return FileVisitResult.CONTINUE;}
   }

   public FileVisitResult visitFileFailed(Path file,
           IOException e) throws IOException{
      return FileVisitResult.TERMINATE;
   }

   public FileVisitResult postVisitDirectory(Path dir,
          IOException e) throws IOException{
       return FileVisitResult.CONTINUE;
   }
}

When you want to do something simple you can use the SimpleFileVisitor and implement only the necessary methods for your functionality.

Observe the changes in a directory

The NIO.2 API offers the WatchService API for monitoring changes. To use the API you need to create an instance of WatchService, register the directory and event type, create a loop to verify the change, retrieve a WatchKey, retrieve all pending events for the WatchKey and do something, reset WatchKey, close the resource if you do not need the WatchServe.

# create
WatchService service = FileSystems.getDefault().newWatchService()

# Register
Paths.get("/home/a/b")
     .register(service, StandardWatchEventKinds.ENTRY_MODIFY)

# Loop
for(;;){/*Handling of events*/}

# Retrieve a WatchKey - determine if an event has occurred
// poll and take mathods
WatchKey key = service.take();

# Processing the WatchKey
for (WatchEvent<?> e: key.pollEvents()){
   // ....
   WatchEvent we = (WatchEvent)e;
   Path p = we.context();
   ....
}

# Resetting the WatchKey - Go out from the infinity loop
if(!key.reset()){break;}

PS: The reset method will not make difference if the WatchKey has been cancelled is ready.
PS: WatchKey States: ready, signalled
https://docs.oracle.com/javase/8/docs/api/java/nio/file/WatchKey.html

A complete example you can see in  Java Tutorials,  this another post show you different ways to use this interface, and the HowToDoInJava post show another great example.

Conclusion

Certainly, the NIO.2 API is a great improvement that helps the files handle. But there is a lot to practices.

A good discussion you can see in the StackOverflow question.

Don’t stop to study. Much other java version is available. Go deeply in your research and pay attention to what the version that you use offer for you.

Related Posts

Reference