An NIO.2 primer--reference

时间:2021-12-20 01:03:23

Part 1: The asynchronous channel APIs

The More New I/O APIs for the Java™ Platform (NIO.2) is one of the major new functional areas in Java 7, adding asynchronous channel functionality and a new file system API to the language. Developers will gain support for platform-independent file operations, asynchronous operations, and multicast socket channels. Part 1 of this two-part article focuses on the asynchronous channel APIs in NIO.2, and Part 2 covers the new file system functionality.

An asynchronous channel represents a connection that supports nonblocking operations, such as connecting, reading, and writing, and provides mechanisms for controlling the operations after they've been initiated. The More New I/O APIs for the Java Platform (NIO.2) in Java 7 enhance the New I/O APIs (NIO) introduced in Java 1.4 by adding four asynchronous channels to the java.nio.channels package:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

These classes are similar in style to the NIO channel APIs. They share the same method and argument structures, and most operations available to the NIO channel classes are also available in the new asynchronous versions. The main difference is that the new channels enable some operations to be executed asynchronously.

The asynchronous channel APIs provide two mechanisms for monitoring and controlling the initiated asynchronous operations. The first is by returning a java.util.concurrent.Future object, which models a pending operation and can be used to query its state and obtain the result. The second is by passing to the operation an object of a new class, java.nio.channels.CompletionHandler, which defines handler methods that are executed after the operation has completed. Each asynchronous channel class defines duplicate API methods for each operation so that either mechanism can be used.

This article, the first in a two-part series on NIO.2, introduces each of the channels and provides some simple examples to demonstrate their use. The examples are available in a runnable state (see Download), and you can try them out on the Java 7 beta releases available from Oracle and IBM® (both still under development at the time of this writing; see Resources). In Part 2, you'll learn about the NIO.2 file system API.

Asynchronous socket channels and futures

To start, we'll look at the AsynchronousServerSocketChannel and AsynchronousSocketChannel classes. Our first example demonstrates how a simple client/server can be implemented using these new classes. First we'll set up the server.

Server setup

An AsychronousServerSocketChannel can be opened and bound to an address similarly to a ServerSocketChannel:

AsynchronousServerSocketChannel server =
AsynchronousServerSocketChannel.open().bind(null);

The bind() method takes a socket address as its argument. A convenient way to find a free port is to pass in a null address, which automatically binds the socket to the local host address and uses a free ephemeral port.

Next, we can tell the channel to accept a connection:

Future<AsynchronousSocketChannel> acceptFuture = server.accept();

This is the first difference from NIO. The accept call always returns immediately, and — unlike ServerSocketChannel.accept(), which returns a SocketChannel — it returns a Future<AsynchronousSocketChannel> object that can be used to retrieve an AsynchronousSocketChannelat a later time. The generic type of the Future object is the result of the actual operation. For example, a read or write returns aFuture<Integer> because the operation returns the number of bytes read or written.

Using the Future object, the current thread can block to wait for the result:

AsynchronousSocketChannel worker = future.get();

Here it blocks with a timeout of 10 seconds:

AsynchronousSocketChannel worker = future.get(10, TimeUnit.SECONDS);

Or it can poll the current state of the operation, and also cancel the operation:

if (!future.isDone()) {
future.cancel(true);
}

The cancel() method takes a boolean flag to indicate whether the thread performing the accept can be interrupted. This is a useful enhancement; in previous Java releases, blocking I/O operations like this could only be aborted by closing the socket.

Client setup

Next, we can set up the client by opening and connecting a AsynchronousSocketChannel to the server:

AsynchronousSocketChannel client = AsynchronousSocketChannel.open();
client.connect(server.getLocalAddress()).get();

Once the client is connected to the server, reads and writes can be performed via the channels using byte buffers, as shown in Listing 1:

Listing 1. Using byte buffers for reads and writes
// send a message to the server
ByteBuffer message = ByteBuffer.wrap("ping".getBytes());
client.write(message).get(); // read a message from the client
worker.read(readBuffer).get(10, TimeUnit.SECONDS);
System.out.println("Message: " + new String(readBuffer.array()));

Scattering reads and writes, which take an array of byte buffers, are also supported asynchronously.

The APIs of the new asynchronous channels completely abstract away from the underlying sockets: there's no way to obtain the socket directly, whereas previously you could call socket() on, for example, a SocketChannel. Two new methods — getOption and setOption — have been introduced for querying and setting socket options in the asynchronous network channels. For example, the receive buffer size can be retrieved by channel.getOption(StandardSocketOption.SO_RCVBUF) instead of channel.socket().getReceiveBufferSize();.

 

Completion handlers

The alternative mechanism to using Future objects is to register a callback to the asynchronous operation. The CompletionHandler interface has two methods:

  • void completed(V result, A attachment) executes if a task completes with a result of type V.
  • void failed(Throwable e, A attachment) executes if the task fails to complete due to Throwable e.

The attachment parameter of both methods is an object that is passed in to the asynchronous operation. It can be used to track which operation finished if the same completion-handler object is used for multiple operations.

Open commands

Let's look at an example using the AsynchronousFileChannel class. We can create a new channel by passing in a java.nio.file.Path object to the static open() method:

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(Paths.get("myfile"));

New open commands for FileChannel

The format of the open commands for asynchronous channels has been backported to the FileChannelclass. Under NIO, a FileChannel is obtained by calling getChannel() on a FileInputStream,FileOutputStream, or RandomAccessFile. With NIO.2, a FileChannel can be created directly using anopen() method, as in the examples shown here.

Path is a new class in Java 7 that we look at in more detail in Part 2. We use thePaths.get(String) utility method to create a Path from a String representing the filename.

By default, the file is opened for reading. The open() method can take additional options to specify how the file is opened. For example, this call opens a file for reading and writing, creates it if necessary, and tries to delete it when the channel is closed or when the JVM terminates:

fileChannel = AsynchronousFileChannel.open(Paths.get("afile"),
StandardOpenOption.READ, StandardOpenOption.WRITE,
StandardOpenOption.CREATE, StandardOpenOption.DELETE_ON_CLOSE);

An alternative open() method provides finer control over the channel, allowing file attributes to be set.

Implementing a completion handler

Next, we want to write to the file and then, once the write has completed, execute something. We first construct a CompletionHandler that encapsulates the "something" as shown in Listing 2:

Listing 2. Creating a completion handler
CompletionHandler<Integer, Object> handler =
new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer result, Object attachment) {
System.out.println(attachment + " completed with " + result + " bytes written");
}
@Override
public void failed(Throwable e, Object attachment) {
System.err.println(attachment + " failed with:");
e.printStackTrace();
}
};

Now we can perform the write:

fileChannel.write(ByteBuffer.wrap(bytes), 0, "Write operation 1", handler);

The write() method takes:

  • ByteBuffer containing the contents to write
  • An absolute position in the file
  • An attachment object that is passed on to the completion handler methods
  • A completion handler

Operations must give an absolute position in the file to read to or write from. It doesn't make sense for the file to have an internal position marker and for reads/writes to occur from there, because the operations can be initiated before previous ones are completed and the order they occur in is not guaranteed. For the same reason, there are no methods in the AsynchronousFileChannel API that set or query the position, as there are in FileChannel.

In addition to the read and write methods, an asynchronous lock method is also supported, so that a file can be locked for exclusive access without having to block in the current thread (or poll using tryLock) if another thread currently holds the lock.

 

Asynchronous channel groups

Each asynchronous channel constructed belongs to a channel group that shares a pool of Java threads, which are used for handling the completion of initiated asynchronous I/O operations. This might sound like a bit of a cheat, because you could implement most of the asynchronous functionality yourself in Java threads to get the same behaviour, and you'd hope that NIO.2 could be implemented purely using the operating system's asynchronous I/O capabilities for better performance. However, in some cases, it's necessary to use Java threads: for instance, the completion-handler methods are guaranteed to be executed on threads from the pool.

By default, channels constructed with the open() methods belong to a global channel group that can be configured using the following system variables:

  • java.nio.channels.DefaultThreadPoolthreadFactory, which defines a java.util.concurrent.ThreadFactory to use instead of the default one
  • java.nio.channels.DefaultThreadPool.initialSize, which specifies the thread pool's initial size

Three utility methods in java.nio.channels.AsynchronousChannelGroup provide a way to create new channel groups:

  • withCachedThreadPool()
  • withFixedThreadPool()
  • withThreadPool()

These methods take either the definition of the thread pool, given as a java.util.concurrent.ExecutorService, or ajava.util.concurrent.ThreadFactory. For example, the following call creates a new channel group that has a fixed pool of 10 threads, each of which is constructed with the default thread factory from the Executors class:

AsynchronousChannelGroup tenThreadGroup =
AsynchronousChannelGroup.withFixedThreadPool(10, Executors.defaultThreadFactory());

The three asynchronous network channels have an alternative version of the open() method that takes a given channel group to use instead of the default one. For example, this call tells channel to use the tenThreadGroup instead of the default channel group to obtain threads when required by the asynchronous operations:

AsynchronousServerSocketChannel channel =
AsynchronousServerSocketChannel.open(tenThreadGroup);

Defining your own channel group allows finer control over the threads used to service the operations and also provides mechanisms for shutting down the threads and awaiting termination. Listing 3 shows an example:

Listing 3. Controlling thread shutdown with a channel group
// first initiate a call that won't be satisfied
channel.accept(null, completionHandler);
// once the operation has been set off, the channel group can
// be used to control the shutdown
if (!tenThreadGroup.isShutdown()) {
// once the group is shut down no more channels can be created with it
tenThreadGroup.shutdown();
}
if (!tenThreadGroup.isTerminated()) {
// forcibly shutdown, the channel will be closed and the accept will abort
tenThreadGroup.shutdownNow();
}
// the group should be able to terminate now, wait for a maximum of 10 seconds
tenThreadGroup.awaitTermination(10, TimeUnit.SECONDS);

The AsynchronousFileChannel differs from the other channels in that, in order to use a custom thread pool, the open() method takes anExecutorService instead of an AsynchronousChannelGroup.

 

Asynchronous datagram channels and multicasting

The final new channel is the AsynchronousDatagramChannel. It's similar to the AsynchronousSocketChannel but worth mentioning separately because the NIO.2 API adds support for multicasting to the channel level, whereas in NIO it is only supported at the level of theMulticastDatagramSocket. The functionality is also available in java.nio.channels.DatagramChannel from Java 7.

An AsynchronousDatagramChannel to use as a server can be constructed as follows:

AsynchronousDatagramChannel server = AsynchronousDatagramChannel.open().bind(null);

Next, we set up a client to receive datagrams broadcast to a multicast address. First, we must choose an address in the multicast range (from 224.0.0.0 to and including 239.255.255.255), and also a port that all clients can bind to:

// specify an arbitrary port and address in the range
int port = 5239;
InetAddress group = InetAddress.getByName("226.18.84.25");

We also require a reference to which network interface to use:

// find a NetworkInterface that supports multicasting
NetworkInterface networkInterface = NetworkInterface.getByName("eth0");

Now, we open the datagram channel and set up the options for multicasting, as shown in Listing 4:

Listing 4. Opening a datagram channel and setting multicast options
// the channel should be opened with the appropriate protocol family,
// use the defined channel group or pass in null to use the default channel group
AsynchronousDatagramChannel client =
AsynchronousDatagramChannel.open(StandardProtocolFamily.INET, tenThreadGroup);
// enable binding multiple sockets to the same address
client.setOption(StandardSocketOption.SO_REUSEADDR, true);
// bind to the port
client.bind(new InetSocketAddress(port));
// set the interface for sending datagrams
client.setOption(StandardSocketOption.IP_MULTICAST_IF, networkInterface);

The client can join the multicast group in the following way:

MembershipKey key = client.join(group, networkInterface);

The java.util.channels.MembershipKey is a new class that provides control over the group membership. Using the key you can drop the membership, block and unblock datagrams from certain addresses, and return information about the group and channel.

The server can then send a datagram to the address and port for the client to receive, as shown in Listing 5:

Listing 5. Sending and receiving a datagram
// send message
ByteBuffer message = ByteBuffer.wrap("Hello to all listeners".getBytes());
server.send(message, new InetSocketAddress(group, port)); // receive message
final ByteBuffer buffer = ByteBuffer.allocate(100);
client.receive(buffer, null, new CompletionHandler<SocketAddress, Object>() {
@Override
public void completed(SocketAddress address, Object attachment) {
System.out.println("Message from " + address + ": " +
new String(buffer.array()));
} @Override
public void failed(Throwable e, Object attachment) {
System.err.println("Error receiving datagram");
e.printStackTrace();
}
});

Multiple clients can also be created on the same port and joined to the multicast group to receive the datagrams sent from the server.

 

Conclusion

NIO.2's asynchronous channel APIs provide a convenient and standard way of performing asynchronous operations platform-independently. They allow application developers to write programs that use asynchronous I/O in a clear manner, without having to define their own Java threads and, in addition, may give performance improvements by using the asynchronous support on the underlying OS. As with many Java APIs, the amount that the API can exploit an OS's native asynchronous capabilities will depend on the support for that platform.

Part 2: The file system APIs

This article completes our two-part introduction to More New I/O APIs for Java (NIO.2) in Java 7. Like the asynchronous channel APIs explored in Part 1, NIO.2's file system APIs fill some significant gaps in the way previous Java versions handle I/O. According to the NIO.2 Java specification request (JSR 203):

The Java platform has long needed a filesystem interface better than the java.io.File class. That class does not handle filenames in a way that works consistently across platforms, it does not support efficient file-attribute access, it does not allow sophisticated applications to take advantage of filesystem-specific features (for example, symbolic links) when available, and many of its methods simply return false on error instead of throwing an informative exception.

Coming to the rescue are three new file system packages in the Java 7 beta:

  • java.nio.file
  • java.nio.file.attribute
  • java.nio.file.spi

This article focuses on the most useful classes in these packages:

  • java.nio.file.Files and java.nio.file.FileVisitor allow you to walk through file systems, querying files or directories up to a certain depth and executing user-implemented callback methods for each one found.
  • java.nio.file.Path and java.nio.file.WatchService allow you to register to "watch" a specific directory. An application watching directories receives notification if files in those directories are created, modified, or deleted.
  • java.nio.attribute.*AttributeView allow you to view file and directory attributes that were previously hidden from Java users. These attributes include file owner and group permissions, access-control lists (ACLs), and extended file attributes.

We'll provide examples that show how to use these classes. The examples are available in a runnable state (see Download), and you can try them out on the Java 7 betas available from IBM® and Oracle (both still under development at the time of this writing; see Resources).

File visitors

Our first example demonstrates the new FileVisitor API.

Imagine a scenario in which you want to traverse a directory tree recursively, stopping at each file and directory under that tree and having your own callback methods invoked for each entry found. In previous Java versions, this would have been a painful process involving recursively listing directories, inspecting their entries, and invoking the callbacks yourself. In Java 7, this is all provided via the FileVisitor API, and using it couldn't be simpler.

The first step is to implement your own FileVisitor class. This class contains the callback methods that the file-visitor engine will invoke as it traverses the file system. The FileVisitor interface consists of five methods, listed here in the typical order they would be called during traversal (T here stands for either java.nio.file.Path or a superclass):

  • FileVisitResult preVisitDirectory(T dir) is called before the entries in that directory are visited. It returns one of theFileVisitResult's enum values to tell the file visitor API what to do next.
  • FileVisitResult preVisitDirectoryFailed(T dir, IOException exception) is called when a directory could not be visited for some reason. The exception that caused the visit to fail is specified in the second parameter.
  • FileVisitResult visitFile(T file, BasicFileAttributes attribs) is called when a file in the current directory is being visited. The attributes for this file are passed into the second parameter. (You'll learn more about file attributes in this article's File attributes section.)
  • FileVisitResult visitFileFailed(T file, IOException exception) is called when the visit to a file has failed. The second parameter specifies the exception that caused the visit to fail.
  • FileVisitResult postVisitDirectory(T dir, IOException exception) is called after the visit to a directory and all its subdirectories has completed. The exception parameter is null when the directory visit has been successful, or it contains the exception that caused the directory visit to end prematurely.

To help developers save time, NIO.2 has kindly provided an implementation of the FileVisitor interface: java.nio.file.SimpleFileVisitor. This class is as basic as it gets: for the *Failed() methods, it just rethrows the exception, and for the other methods it continues without doing anything at all! What's useful about this class is that you can use anonymous classes to override only the methods you want; the rest of the methods are implemented by default.

Listing 1 shows how we create an example FileVisitor instance:

Listing 1. FileVisitor implementation
FileVisitor<Path> myFileVisitor = new SimpleFileVisitor<Path>() { 

    @Override
public FileVisitResult preVisitDirectory(Path dir) {
System.out.println("I'm about to visit the "+dir+" directory");
return FileVisitResult.CONTINUE;
} @Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attribs) { System.out.println("I'm visiting file "+file+" which has size " +attribs.size());
return FileVisitResult.CONTINUE;
} };

The FileVisitor implementation in Listing 1 should print a message for each directory and file it visits and also give the size of the files from their BasicFileAttributes.

Next we want to create a Path from which to start our file visiting. This is done using the java.nio.Paths class:

Path headDir = Paths.get("headDir");

We can use either of two methods on the java.nio.Files class to start the tree traversal:

  • public static void walkFileTree(Path head, FileVisitor<? super Path> fileVisitor) walks the file tree under the head directory, invoking the callback methods implemented in fileVisitor as it goes.
  • public static void walkFileTree(Path head, Set<FileVisitOption> options, int depth, FileVisitor<? super Path> fileVisitor) is similar to the preceding method but gives you two additional parameters to specify visit options and how many directories deep into the file tree the traversal should go.

We'll use the simpler version of the walkFileTree() method to start the process of walking the file tree:

Files.walkFileTree(headDir, myFileVisitor);

Suppose the directory structure looks like this:

 headDir
|--- myFile1
|--- mySubDirectory1
| \myFile2
\--- mySubDirectory2
|--- myFile3
\--- mySubdirectory3
\---myFile4

Listing 2 shows the output from this example:

Listing 2. FileVisitor output
I'm about to visit the headDir directory
I'm about to visit the headDir\mySubDirectory2 directory
I'm about to visit the headDir\mySubDirectory2\mySubDirectory3 directory
I'm visiting file headDir\mySubDirectory2\mySubDirectory3\myFile4 which has size 2
I'm visiting file headDir\mySubDirectory2\myFile3 which has size 2
I'm about to visit the headDir\mySubDirectory1 directory
I'm visiting file headDir\mySubDirectory1\myFile2 which has size 2
I'm visiting file headDir\myFile1 which has size 2

As you can see, the file traversal is depth first but not necessarily in any alphabetical order within a directory. Our callback methods were invoked as expected, and we can see that all the files in the tree have been listed and all the directories have been visited.

In only about 15 lines, we've created a file visitor that will walk any file tree you give it and inspect the files contained therein. This example is basic, but the callbacks can be implemented to be as complex as you want.

 

Directory watching

This second example covers the exciting world of the new WatchService API and its associated classes.

The scenario for this example is simple: You'd like to track whether any files or directories in a particular directory (or directories) are being created, modified, or deleted. You might use this information to update a file listing in a GUI display or perhaps to detect the modification of configuration files that could then be reloaded. In previous Java versions, you must implement an agent running in a separate thread that keeps track of all the contents of the directories you wish to watch, constantly polling the file system to see if anything relevant has happened. In Java 7, the WatchService API provides the ability to watch directories. It removes all the complexity of writing your own file system poller and, where possible, is based upon existing native system APIs for better performance.

The first step is to create a WatchService instance via the java.nio.file.FileSystems class. We won't go into the details of file systems in this article, so just know that in most cases you'll want to get the default file system and then invoke its newWatchService() method:

WatchService watchService = FileSystems.getDefault().newWatchService();

Now that we have our watch service instance, we want to register a path to watch. We create a Path object for the directory we wish to watch in a slightly different way from the file visitor example, so that we can use its File instance again later:

File watchDirFile = new File("watchDir");
Path watchDirPath = watchDirFile.toPath();

The Path class implements the java.nio.file.Watchable interface, and that interface defines the register() method we'll be using in this example. WatchKey register(WatchService watchService, WatchEvent.Kind<?>... events) registers the Path this method is called on with the specified watchService for the specific events given. Events trigger a notification only if they are specified in the register call.

For the default WatchService implementation, the java.nio.file.StandardWatchEventKind class defines three static implementations ofwatchEvent.Kind that can be used in the register() calls:

  • StandardWatchEventKind.ENTRY_CREATE indicates that a file or directory has been created within the registered Path. An ENTRY_CREATEevent is also triggered when a file is renamed or moved into this directory.
  • StandardWatchEventKind.ENTRY_MODIFY indicates that a file or directory in the registered Path has been modified. Exactly which events constitute a modification is somewhat platform-specific, but suffice it to say that actually modifying the contents of a file always triggers a modify event. On some platforms, changing attributes of files can also trigger this event.
  • StandardWatchEventKind.ENTRY_DELETE indicates that a file or directory has been deleted from the registered Path. An ENTRY_DELETE event is also triggered when a file is renamed or moved out of this directory.

For our example, let's watch the ENTRY_CREATE and ENTRY_MODIFY events, but not ENTRY_DELETE:

WatchKey watchKey = watchDirPath.register(watchService,
StandardWatchEventKind.ENTRY_CREATE, StandardWatchEventKind.ENTRY_MODIFY);

Our Path is now registered to be watched, and the WatchService will work away silently in the background, watching that directory intently. The same WatchService instance can watch multiple directories by using the same Path creation and register() calls we've shown above.

The observant among you may have spotted that the register() method call returns a class we haven't come across before: WatchKey. This class represents your registration with the WatchService. Whether you hang onto this reference or not is up to you, because the WatchServicereturns the relevant WatchKey to you when an event is triggered. However, do note that there are no method calls to find out which directory theWatchKey was registered with, so if you're watching multiple directories you may wish to track which WatchKeys go with which Paths. When you're done with a particular WatchKey and the events it's registered for, you can cancel its registration with the WatchService simply by calling its cancel() method.

Now that our Path is registered, we can check in with the WatchService at our convenience to see if any of the events we were interested in has occurred. WatchService provides three methods for checking if anything exciting has happened:

  • WatchKey poll() returns the next WatchKey that has had some of its events occur, or null if no registered events have happened.
  • WatchKey poll(long timeout, TimeUnit unit) takes a timeout and time units (java.util.concurrent.TimeUnit). If an event occurs during the specified time period, this method exits, returning the relevant WatchKey. If there are no WatchKeys to return by the end of the timeout, this method returns null.
  • WatchKey take() is similar to the preceding methods, except it will wait indefinitely until a WatchKey is available to return.

Once a WatchKey has been returned by one of these three methods, it will not be returned by a further poll() or take() call until its reset()method is invoked, even if events it is registered for occur. Once a WatchKey is returned by the WatchService, you can inspect its events that have been triggered by calling the WatchKey's pollEvents() method, which returns a List of WatchEvents.

To illustrate, the simple example in Listing 3 continues from the WatchKey we registered earlier:

Listing 3. Using pollEvents()
// Create a file inside our watched directory
File tempFile = new File(watchDirFile, "tempFile");
tempFile.createNewFile(); // Now call take() and see if the event has been registered
WatchKey watchKey = watchService.take();
for (WatchEvent<?> event : watchKey.pollEvents()) {
System.out.println(
"An event was found after file creation of kind " + event.kind()
+ ". The event occurred on file " + event.context() + ".");
}

When executed, the code in Listing 3 prints:

An event was found after file creation of kind ENTRY_CREATE. The event occurred
on file tempFile.
An event was found after file creation of kind ENTRY_MODIFY. The event occurred
on file tempFile.

As you can see, we got the ENTRY_CREATE event for the newly created tempFile as expected, but we also got another event. On some operating systems, a file creation or deletion can sometimes also produce an ENTRY_MODIFY event. If we had not registered to receive modification events, then we would have only gotten the ENTRY_CREATE event, whatever the OS.

Extended sample code (demonstrating file modification and deletion for the registered WatchKey in this section's example) is included in the sample code download.

 

File attributes

Our third and final example covers the new APIs for getting and setting file attributes using the classes under the java.nio.file.attributepackage.

The new APIs offer access to more file attributes than you can imagine. In previous Java releases, you can get only a basic set of file attributes (size, modification time, whether the file is hidden, and whether it is a file or directory). To get or modify any further file attributes, you must implement this yourself in native code specific to the platforms you want to run on — not exactly easy. Brilliantly, Java 7 should allow you to read and, where possible, modify an extended set of attributes in a simple way via the java.nio.file.attribute classes, completely abstracting away the platform-specific nature of these operations.

Seven attribute views are available in the new APIs, some of which are specific to the operating system. These "view" classes allow you to get and set whichever attributes they are associated with, and each one has a counterpart attribute class that contains the actual attribute information. Let's take a look at them in turn.

AclFileAttributeView and AclEntry

AclFileAttributeView allows you to get and set the ACL and file-owner attributes of a particular file. Its getAcl() method returns a List ofAclEntry objects, one for each permission set on the file. Its setAcl(List<AclEntry>) method allows you to modify that access list. This attribute view is only available for Microsoft® Windows® systems.

BasicFileAttributeView and BasicFileAttributes

This view class allows you to get a set of — no surprise — basic file attributes, building on those available in previous Java versions. ItsreadAttributes() method returns a BasicFileAttributes instance containing details of last modified time, last access time, creation time, size, and type of file (regular file, directory, symbolic link, or other). This attribute view is available on all platforms.

Let's look at an example of this view. To get a file attribute view for a particular file we start, as always, by creating a Path object for the file we're interested in:

 File attribFile = new File("attribFile");
Path attribPath = attribFile.toPath();

To get the file attribute view we want, we'll use the getFileAttributeView(Class viewClass) method on Path. To get theBasicFileAttributeView for attribPath, we simply call:

BasicFileAttributeView basicView
= attribPath.getFileAttributeView(BasicFileAttributeView.class);

As described earlier, to get the BasicFileAttributes from BasicFileAttributeView, we just call its readAttributes() method:

BasicFileAttributes basicAttribs = basicView.readAttributes();

And that's it. Now you have all of the basic file attributes for that file to do whatever you wish with. For the BasicFileAttributes, only the creation, last-modified, and last-access times can be altered (because it wouldn't make sense to change the size or type of file). To change these, we can use the java.nio.file.attribute.FileTime class to create a new time and then call the setTimes() method onBasicFileAttributeView. For example, we could move the last-modified time for our file a minute further into the future:

FileTime newModTime
= FileTime.fromMillis(basicAttribs.lastModifiedTime().toMillis() + 60000);
basicView.setTimes(newModTime, null, null);

The two nulls indicate that we do not want to change the last access time or creation time for this file. If you check the basic attributes again, in the same way we did earlier, you should see that the last modified time has been altered but the creation time and last access time have remained the same.

DosFileAttributeView and DosFileAttributes

This view class allows you to get attributes specific to DOS. (As you might guess, this view is for Windows systems only.) Its readAttributes()method returns a DosFileAttributes instance containing details of whether the file in question is read-only, hidden, a system file, and an archive. The view also has set*(boolean) methods for each of these properties.

FileOwnerAttributeView and UserPrincipal

This view class allows you to get and set the owner of a particular file. Its getOwner() method returns a UserPrincipal (also in thejava.nio.file.attribute package), which in turn has a getName() method returning a String containing the owner's name. The view also provides a setOwner(UserPrincipal) method allowing you to change a file's owner. This view is available on all platforms.

FileStoreSpaceAttributeView and FileStoreSpaceAttributes

This catchily named class allows you to get information about a particular file store. Its readAttributes() method returns aFileStoreSpaceAttributes instance containing details of the total space, the unallocated space, and the usable space on the file store. This view is available on all platforms.

PosixFileAttributeView and PosixFileAttributes

This view class, available on UNIX® systems only, allows you to get and set attributes specific to POSIX (Portable Operating System Interface). Its readAttributes() method returns a PosixFileAttributes instance containing details of the owner, group owner, and file permissions for this file (those you would normally set using the UNIX chmod command). The view also provides setOwner(UserPrincipal),setGroup(GroupPrincipal), and setPermissions(Set<PosixFilePermission>) methods to modify these attributes.

UserDefinedFileAttributeView and String

This view class, available only on Windows, allows you to get and set extended attributes on files. These attributes are unlike the others in that they are just name-value pairs and can be set to anything you wish. This can be useful if you want to add some hidden metadata to a file without altering the file's content. The view provides a list() method that returns a List of String names of the extended attributes for the relevant file.

To get the contents of a particular attribute once you have its name, the view has a size(String name) method to return the size of the attribute's value and a read(String name, ByteBuffer dest) method to read the attribute value into the ByteBuffer. The view also provides awrite(String name, ByteBuffer source) method to create or alter an attribute, and also a delete(String name) method to remove an existing attribute entirely.

This is probably the most interesting new attribute view because it allows you to add attributes with arbitrary String names and ByteBuffervalues to files. That's right — its value is a ByteBuffer, so you can store any binary data in there you want.

First, we'll get the attribute view:

UserDefinedFileAttributeView userView
= attribPath.getFileAttributeView(UserDefinedFileAttributeView.class);

To get a list of the user-defined attribute names for this file, we call the list() method on the view:

List<String> attribList = userView.list();

Once we have a particular attribute name we wish to get the associated value for, we allocate a ByteBuffer of the right size for the value and then call the view's read(String, ByteBuffer) method:

ByteBuffer attribValue = ByteBuffer.allocate(userView.size(attribName));
userView.read(attribName, attribValue);

attribValue now contains whatever data was stored for that particular attribute. To set your own attribute, you simply need to create yourByteBuffer and fill it with whatever data you wish and then call the write(String, ByteBuffer) method on the view:

userView.write(attribName, attribValue);

Writing an attribute either creates that attribute or overwrites an existing attribute with the same name.

And with that, we conclude the third and final example. Full example code demonstrating four of the attribute views (BasicFileAttributeView,FileOwnerAttributeViewFileStoreSpaceAttributeView, and UserDefinedAttributeView) is included in the sample code download.

 

Conclusion

In addition to the topics covered in this article are a number of other NIO.2 file APIs. With Java 7 comes the new ability to create, inspect, and modify symbolic links. There are also new classes allowing access to lower-level information about the file system, and even to supply providers (called FileSystem and FileStore) to access any type of file system you wish.

In summary, NIO.2 gives Java developers a simple, consistent, and powerful set of APIs to interact with the file system. Its aim is to abstract away some of the complex platform-specific details of dealing with files and directories and to give programmers more power and flexibility, and it has achieved this well.

http://www.ibm.com/developerworks/java/library/j-nio2-1/index.html

http://www.ibm.com/developerworks/java/library/j-nio2-2/index.html?ca=dat