A Guide To NIO2 Asynchronous File Channel

1. Overview

In this article, we are going to explore one of the key additional APIs of the new I/O (NIO2) in Java 7, asynchronous file channel APIs.

If you are new to asynchronous channel APIs in general, we have an introductory article on this site which you can read by following this link before proceeding.

You can read more about NIO.2 file operations and path operations as well – understanding these will make this article much easier to follow.

To use the NIO2 asynchronous file channels in our projects, we have to import the java.nio.channels package as it bundles all required classes:

import java.nio.channels.*;

2. The AsynchronousFileChannel

In this section, we will explore how to use the main class that enables us to perform asynchronous operations on files, the AsynchronousFileChannel class. To create an instance of it, we call the static open method:

Path filePath = Paths.get("/path/to/file");

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
  filePath, READ, WRITE, CREATE, DELETE_ON_CLOSE);

All enum values come from the StandardOpenOption.

The first parameter to the open API is a Path object representing the file location. To read more about path operations in NIO2, follow this link. The other parameters make up a set specifying options that should be available to the returned file channel.

The asynchronous file channel we have created can be used to perform all known operations on a file. To perform only a subset of the operations, we would specify options for only those. For instance, to only read:

Path filePath = Paths.get("/path/to/file");

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
  filePath, StandardOpenOption.READ);

3. Reading From a File

Just like with all asynchronous operations in NIO2, reading a file’s contents can be done in two ways. Using Future and using CompletionHandler. In each case, we use the read API of the returned channel.

Inside the test resources folder of maven or in the source directory if not using maven, let’s create a file called file.txt with only the text baeldung.com at it’s beginning. We will now demonstrate how to read this content.

3.1. The Future Approach

First, we will see how to read a file asynchronously using the Future class:

@Test
public void givenFilePath_whenReadsContentWithFuture_thenCorrect() {
    Path path = Paths.get(
      URI.create(
        this.getClass().getResource("/file.txt").toString()));
    AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
      path, StandardOpenOption.READ);

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    Future<Integer> operation = fileChannel.read(buffer, 0);

    // run other code as operation continues in background
    operation.get();

    String fileContent = new String(buffer.array()).trim();
    buffer.clear();

    assertEquals(fileContent, "baeldung.com");
}

In the above code, after creating a file channel, we make use of the read API – which takes a ByteBuffer to store the content read from the channel as its first parameter.

The second parameter is a long indicating the position in the file from which to start reading.

The method returns right away whether the file has been read or not.

Next, we can execute any other code as the operation continues in the background. When we are done with executing other code, we can call the get() API which returns right away if the operation already completed as we were executing other code, or else it blocks until the operation completes.

Our assertion indeed proves that the content from the file has been read.

If we had changed the position parameter in the read API call from zero to something else, we would see the effect too. For example, the seventh character in the string baeldung.com is g. So changing the position parameter to 7 would cause the buffer to contain the string g.com.

3.2. The CompletionHandler Approach

Next, we will see how to read a file’s contents using a CompletionHandler instance:

@Test
public void
  givenPath_whenReadsContentWithCompletionHandler_thenCorrect() {

    Path path = Paths.get(
      URI.create( this.getClass().getResource("/file.txt").toString()));
    AsynchronousFileChannel fileChannel
      = AsynchronousFileChannel.open(path, StandardOpenOption.READ);

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    fileChannel.read(
      buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {

        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            // result is number of bytes read
            // attachment is the buffer containing content
        }
        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {

        }
    });
}

In the above code, we use the second variant of the read API. It still takes a ByteBuffer and the start position of the read operation as the first and second parameters respectively. The third parameter is the CompletionHandler instance.

The first generic type of the completion handler is the return type of the operation, in this case, an Integer representing the number of bytes read.

The second is the type of the attachment. We have chosen to attach the buffer such that when the read completes, we can use the content of the file inside the completed callback API.

Semantically speaking, this is not really a valid unit test since we cannot do an assertion inside the completed callback method. However, we do this for the sake of consistency and because we want our code to be as copy-paste-run-able as possible.

4. Writing to a File

Java NIO2 also allows us to perform write operations on a file. Just as we did with other operations, we can write to a file in two ways. Using Future and using CompletionHandler. In each case, we use the write API of the returned channel.

Creating an AsynchronousFileChannel for writing to a file can be done like this:

AsynchronousFileChannel fileChannel
  = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);

4.1. Special Considerations

Notice the option passed to the open API. We can also add another option StandardOpenOption.CREATE if we want the file represented by a path to be created in case it does not already exist. Another common option is StandardOpenOption.APPEND which does not over-write existing content in the file.

We will use the following line for creating our file channel for test purposes:

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
  path, WRITE, CREATE, DELETE_ON_CLOSE);

This way, we will provide any arbitrary path and be sure that the file will be created. After the test exits, the created file will be deleted. To ensure the files created are not deleted after the test exits, you can remove the last option.

To run assertions, we will need to read the file content where possible after writing to them. Let’s hide the logic for reading in a separate method to avoid redundancy:

public static String readContent(Path file) {
    AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
      file, StandardOpenOption.READ);

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    Future<Integer> operation = fileChannel.read(buffer, 0);

    // run other code as operation continues in background
    operation.get();

    String fileContent = new String(buffer.array()).trim();
    buffer.clear();
    return fileContent;
}

4.2. The Future Approach

To write to a file asynchronously using the Future class:

@Test
public void
  givenPathAndContent_whenWritesToFileWithFuture_thenCorrect() {

    String fileName = UUID.randomUUID().toString();
    Path path = Paths.get(fileName);
    AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
      path, WRITE, CREATE, DELETE_ON_CLOSE);

    ByteBuffer buffer = ByteBuffer.allocate(1024);

    buffer.put("hello world".getBytes());
    buffer.flip();

    Future<Integer> operation = fileChannel.write(buffer, 0);
    buffer.clear();

    //run other code as operation continues in background
    operation.get();

    String content = readContent(path);
    assertEquals("hello world", content);
}

Let’s inspect what is happening in the above code. We create a random file name and use it to get a Path object. We use this path to open an asynchronous file channel with the previously mentioned options.

We then put the content we want to write to the file in a buffer and perform the write. We use our helper method to read the contents of the file and indeed confirm that it is what we expect.

4.3. The CompletionHandler Approach

We can also use the completion handler so that we don’t have to wait for the operation to complete in a while loop:

@Test
public void
  givenPathAndContent_whenWritesToFileWithHandler_thenCorrect() {

    String fileName = UUID.randomUUID().toString();
    Path path = Paths.get(fileName);
    AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
      path, WRITE, CREATE, DELETE_ON_CLOSE);

    ByteBuffer buffer = ByteBuffer.allocate(1024);
    buffer.put("hello world".getBytes());
    buffer.flip();

    fileChannel.write(
      buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {

        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            // result is number of bytes written
            // attachment is the buffer
        }
        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {

        }
    });
}

When we call the write API this time, the only new thing is a third parameter where we pass an anonymous inner class of type CompletionHandler.

When the operation completes, the class calls it’s completed method within which we can define what should happen.

5. Conclusion

In this article, we have explored some of the most important features of the Asynchronous File Channel APIs of Java NIO2.

To get all code snippets and the full source code for this article, you can visit the Github project.

Leave a Reply

Your email address will not be published.