Tuesday 6 January 2015

Java File I/O (NIO.2)

Background

Initially all Java had for supporting Files was java.io.File class. But it lacked lot of functional features like copying file, handling symbolic links, getting attributes of file etc. As a result new set of I/O APIs were introduced in Java 7 called NIO.2 (New I/O).



We will see three important things in this post
  1. Path interface and it's functions
  2. FileVisitor (Walking a file tree)
  3. Directory WatchService (Watching a directory for changes)

 Understanding Path interface

Path is an interface in java.nio.file package. It extends 3 other interfaces
  1. Comparable<Path>
  2. Iterable<Path>
  3. Watchable
You can get instance of Path using methods defines in class java.nio.file.Paths . Following is the code to understand Path interface and it's methods -

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class HelloWorld {

    public static void main(String args[]) {

        Path p1 = Paths.get("C:\\Users\\.\\athakur\\Desktop\\..\\Desktop");
        Path p3= Paths.get("C:\\Users\\.\\athakur\\Desktop\\..\\Desktop\\abc.txt");
        Path p4 = Paths.get("C:\\Users\\athakur\\Desktop\\abc.txt");
        
        //GET methods
        System.out.println("File name : " + p1.getFileName());
        System.out.println("Root File : " + p1.getRoot());
        System.out.println("Name Count : " + p1.getNameCount());
        System.out.println("Get Name: " + p1.getName(2)); // thows java.lang.IllegalArgumentException if index is > getNameCount() - 1
        System.out.println("Get Parent: " + p1.getParent());
        
        for(Path p : p1) {
            System.out.println("Element : " + p);
        }
        
        //TO methods
        System.out.println("Uri : " + p1.toUri());
        System.out.println("File : " + p1.toFile());
        System.out.println("Normalize : " + p1.normalize());
        System.out.println("Absoulute  : " + p1.toAbsolutePath()); //absolute does not normalize
        
        try {
            System.out.println("RealPath  : " + p1.toRealPath());// Throw IOException if File does not exist
            //returns absolute normalized path
        } catch (IOException e) {
            e.printStackTrace();
        } 
        
        System.out.println(p3.equals(p4));
        System.out.println(p3.toAbsolutePath().equals(p4.toAbsolutePath()));
        
        try {
            System.out.println(p3.toRealPath().equals(p4.toRealPath()));
            //Make sure Paths are normalized and absolute before comparing
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        System.out.println("File exists : " + Files.exists(p1));
        
    }


}


Output :

File name : Desktop
Root File : C:\
Name Count : 6
Get Name: athakur
Get Parent: C:\Users\.\athakur\Desktop\..
Element : Users
Element : .
Element : athakur
Element : Desktop
Element : ..
Element : Desktop
Uri : file:///C:/Users/./athakur/Desktop/../Desktop/
File : C:\Users\.\athakur\Desktop\..\Desktop
Normalize : C:\Users\athakur\Desktop
Absoulute  : C:\Users\.\athakur\Desktop\..\Desktop
RealPath  : C:\Users\athakur\Desktop
false
false
true
true

Another interesting method is relativize()

        Path p5 = Paths.get("C:\\Users\\athakur\\Desktop");
        Path p6 = Paths.get("C:\\Users\\athakur\\Desktop\\Dir1\\abc.txt");
        
        System.out.println("p5 relative to p6 : " + p6.relativize(p5));
        System.out.println("p6 relative to p5 : " + p5.relativize(p6));


Output :

p5 relative to p6 : ..\..
p6 relative to p5 : Dir1\abc.txt

NOTE:
  • The relativize() method needs that both paths be relative or both be absolute. IllegalArgumentException will be throws if a relative path is used with an absolute path and vice versa.

You can also use resolve() method to create a new Path. The object on which resolve() is invoked forms the base of the new Path and the input is appended to it. Eg.

Path path1 = Paths.get("C:\\Users\\athakur\\Desktop");
Path path2 = Paths.get("abc.txt");
System.out.println(path1.resolve(path2));


Output :
C:\\Users\\athakur\\Desktop\\abc.txt

 NOTE:
  • In above example input argument was a relative path but if it is an absolute path the base is ignored and the copy of absolute argument Path is returned.
  • Like relativize() , resolve() also does not cleanup up path symbols like '..' or '.'. You can clean it up with normalize() though. Again normalize() does not check if file actually exists. That's the job for toRealPath().


Note some of the following important points
  1. Most of the methods in Path interface do not throw IOException if underlying file or directory does not exist except the method toRealPath(). That's because Path just is a representation of file in the file system not the actual file.
  2. To carry out functionality like creating/deleting directories, copying you can use methods provided in java.nio.file.Files class.
  3. toRealPath() method normalizes the path as well and converts to absolute path and returns.
  4. java.io.File class has toPath() method that return Path instance from a File instance. Similarly Path has toFile() method to get File instance.
  5. You can also get Path instance from underlying file system as -
    • FileSystems.getDefault().getPath("C:\\Users\\athakur\\Desktop") OR
    • FileSystems.getDefault().getPath("C:","Users","athakur","Desktop")
  6. Also note the Root of a file 'c:/' in above case is never counted as path element. getName(index) method will return element at position= index and position starts from 0 at element after root. So to sum up getName(int index) method is zero indexed and file systems root excluded.
  7. You can compare two paths with compareTo() method [as Path interface extends Comparable interface] or you can use equals(). compareTo() method will  compare paths character by character where as equals() will check if two paths are pointing to same file. However note for equals() to work each path should be normalized and converted to absolute path (you can use toRealPath()). Or you can simply use Files.isSameFile(path1, path2) 
  8. isSameFile() method will first check if two Paths are same irrespective of files actually exist or not. If they are same true is returned. If it returns false then the files are actually located and compared. If files does not exist then IOException is thrown. 

Below diagram summaries general way to create a Path instance.
NOTE :
  • Unlike File class Path interface contains supports for symbolic links.
  • Path is an interface. You cannot directly create a instance of Path and have to use one of the factory methods provided. This is done deliberately since files are system dependent and by using a factory method Java handles system specific details for you.
  • You can create a Path variable using a varagrg String arguments like Paths.get(String,String,...). Benefit of this is that you dont have to worry about the separator which is platform dependent. You can also get it as System.getProperty("path.separator")
  • Path object is not a file but a representation of a location within your file system. So you can perform operations like getting the root directory or parent directory without the file actually existing. However some methods like Path.toRealPath() do need file to exist and throw checked exception.
  • getRoot() method returns root element of the Path object or null of the Path is relative.
Notes :
  1. Files.isDirectory() follows links by default
  2. Files.deleteIfExists() would throw a DirectoryNotEmptyException if it had contents. It will work for empty directory or a symbolic link.
  3. Files.lines() returns a Stream<String> and Files.readAllLines() returns a List<String>. Files.lines() reads the file in a lazy manner, while Files.readAllLines() reads the entire file into memory all at once; therefore Files.lines() works better on large files with limited memory available
  4. Also see - Walking a directory using Streams API in java8 

Walking a File Tree (walkFileTree)

NOTE : Below method is for java 7. For Java 8 we have Files.walk(path) method that returns Stream<Path> which you can iterate on in a depth first pattern. See Walking a directory using Streams API in java8 

java.nio.file.Files class has two method that are provided to traverse File tree. They are - 

  • Path walkFileTree(Path start, FileVisitor<? super Path> visitor)
  • Path walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor)

Both methods take instance of Path to start with and an instance of  FileVisitor that describes what action to taken when each file is visited.

FileVisitor interface provides various methods to take actions at various junctions. Methods are -

  1. visitFile()
  2. preVisitDirectory()
  3. postVisitDirectory()
  4. visitFileFailed()
You need to create a class that implements interface java.nio.file.FileVisitor . Or simply you can extends SimpleFileVisitor class and override necessary methods.

import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;


public class TestFileVisitor extends SimpleFileVisitor<Path> {
    
    public FileVisitResult visitFile(Path path, BasicFileAttributes fileAttributes) {
        System.out.println("Visiting File :" + path.getFileName());
        return FileVisitResult.CONTINUE;
    }

    public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes fileAttributes) {
        System.out.println("Accessiong Directory : " + path );
        return FileVisitResult.CONTINUE;
    }
    
    public static void main(String args[])
    {
        Path startPath = Paths.get("C:\\Users\\athakur\\Desktop\\Dir1");
        System.out.println("Staring to traverse File tree");
        try {
            Files.walkFileTree(startPath, new TestFileVisitor());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}


Output : 

Staring to traverse File tree
Accessiong Directory : C:\Users\athakur\Desktop\Dir1
Accessiong Directory : C:\Users\athakur\Desktop\Dir1\Dir2
Visiting File :File2 - Copy.txt
Visiting File :File2.txt
Accessiong Directory : C:\Users\athakur\Desktop\Dir1\Dir3
Visiting File :File3 - Copy.txt
Visiting File :File3.txt
Visiting File :File1 - Copy.txt
Visiting File :File1.txt

Note :
  1. You can use walkFileTree() to copy entire directory from source to destination as simple Files.copy() will just copy the directory contents not the files withing the directory of given directory [shallow copy].
  2. You can use PathMatcher to match a file. The PathMatcher interface is implemented for each file system, and you can get an instance of it from the FileSystem class using the getPathMatcher() method. Eg .

    String pattern = "glob:File*.java";
    matcher = FileSystems.getDefault().getPathMatcher(pattern);
    System.out.println(matcher.matches(path.getFileName()));

  3. Glob is a pattern-specifying mechanism where you can specify file matching patterns as strings. It's a subset of regex.
  4. postVisitDirectory() will be invoked after all files and directories of a current directory are visited including all files of it's child directories.

 Directory WatchService

By using WatchService you can watch over for changes in files corresponding to a directory. Following program checks if file is changes using WatchService -

import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;


public class TestFileWatchService {

    public static void main(String args[]) throws IOException, InterruptedException {
        Path p = Paths.get("C:\\Users\\athakur\\Desktop\\Dir1");
        WatchService ws = p.getFileSystem().newWatchService();
        p.register(ws, StandardWatchEventKinds.ENTRY_MODIFY);
        
        //infinite loop
        while(true)
        {
            WatchKey watchKey = null;
            watchKey = ws.take();
            for(WatchEvent<?> event : watchKey.pollEvents()){
                switch(event.kind().name()){
                    case "OVERFLOW":
                        System.out.println("Some events were lost");
                        break;
                    case "ENTRY_MODIFY":
                        System.out.println("File " + event.context() + " is changed!");
                        break;
                }
            }
            watchKey.reset();//reset the key to get further notifications
        }
    }
    
}

Now try changing any file in the directory you should see output like - 
File File1.txt is changed!
File File1.txt is changed!
File File1.txt is changed!


Few  important points to note here :
  1. You can also create new WatchService using FileSystems.getDefault().newWatchService().
  2. take() method is a blocking call. It will wait till a WatchKey is available where as the poll() method that can also be used to get key is a non blocking call i.e it returns immediately if key is not available.
  3. Once events are polled from the key do not forget to reset the key or else you will not get subsequent event notifications.
  4. Your program may receive an OVERFLOW event even if the path is not registered with some watch service for some events.
  5. Only files in given directory are watched not the ones in subdirectories.
  6. If Path does not correspond to a directory you will get following Exception :

    Exception in thread "main" java.nio.file.NotDirectoryException: C:\Users\athakur\Desktop\abc.txt
        at sun.nio.fs.WindowsWatchService$Poller.implRegister(Unknown Source)
        at sun.nio.fs.AbstractPoller.processRequests(Unknown Source)
        at sun.nio.fs.WindowsWatchService$Poller.run(Unknown Source)
        at java.lang.Thread.run(Unknown Source)

Related Links

t> UA-39527780-1 back to top