Home » Core Java » nio » Java Nio Tutorial for Beginners

About JJ

Jean-Jay Vester graduated from the Cape Peninsula University of Technology, Cape Town, in 2001 and has spent most of his career developing Java backend systems for small to large sized companies both sides of the equator. He has an abundance of experience and knowledge in many varied Java frameworks and has also acquired some systems knowledge along the way. Recently he has started developing his JavaScript skill set specifically targeting Angularjs and also bridged that skill to the backend with Nodejs.

Java Nio Tutorial for Beginners

This article is a beginners tutorial on Java NIO (New IO). We will take a high level look at this API which provides an alternative to Java IO. The Java NIO API can be viewed here. The example code demonstrates use of the core abstractions in this topic.

The example code in this article was built and run using:

  • Java 1.8.101 (1.8.x will do fine)
  • Maven 3.3.9 (3.3.x will do fine)
  • Spring source tool suite 4.6.3 (Any Java IDE would work)
  • Ubuntu 16.04 (Windows, Mac or linux)

 
 

1. Introduction

Since Java 1.4 the Java NIO API has provided an alternate method of dealing with IO operations. Why did we need an alternate method for doing IO? As time progresses new problem sets arrive and new approaches to solving these problems are thought of. To understand the need for an alternate means of IO handling one should probably understand the core differences between the two approaches.

IO

NIO

Core differences:Core differences:
Stream oriented processingUses buffers
Blocking in processingNon blocking in processing
Good for:Good for:
High data volume with low simultaneous open file descriptor counts

(eg: less client connections with more data chunks per connection)

Less data volume with high simultaneous open file descriptor counts

(eg: More connections with smaller / infrequent “chunks” of data)

NIO puts us in a position to make more judicious use of server / machine resources. By bringing us closer to the metal with an intelligent selection of abstractions we are able to better apply finite server resources to meet the increasing demands of modern day scale.

2. Java NIO

A quick glance at the summary of the Java NIO API reveals to us the core abstractions one should be familiar with when working with Java NIO. These are:

  • Buffers       : A container to hold data for the purposes of reading and or writing.
  • Channels   : An abstraction for dealing with an open connection to some component that is performing some kind of IO operation at a hardware level.
  • Charsets    : Contains charsets, decoders and encoders for translating between bytes and unicode.
  • Selectors   : A means to work with multiple channels via one abstraction.

2.1 Buffers

Shows the Buffer class hierarchy

Buffer is a container for a fixed size of data of a specific primitive type (char, byte, int, long, float etc). A Buffer has content, a position, a limit and capacity. It can flip, rewind, mark and reset its position reinforcing the core differences between NIO and IO (buffer vs stream).

  • Capacity = number of elements it contains.
  • Limit = index of element that must not be read or written.
  • Position = index of next element to read or write.
  • Flip = invert position and limit when toggling the IO operation on a Buffer. (eg: write out to console what we just read from a Channel into the Buffer).
  • Rewind = sets position to 0 and leaves limit unchanged in order to re-read the Buffer.
  • Mark = bookmarks a position in the Buffer.
  • Reset = resets the position to the previous mark.

What does all that mean? Well basically we put content into a Buffer (either read it from a Channel or put it directly into the Buffer with the intent to write it to a Channel).

We then advance the cursor through the content of the Buffer  as we read or write. We flip a Buffer to change our IO operation on the Buffer (ie: go from reading to writing).

The capacity represents the total capacity the Buffer can hold with regard to content. The actual metric used for measurement depends on the type of the Buffer. (eg: CharBuffer capacity measured in characters and ByteBuffer capacity measured in Bytes).

2.1.1 Example usage of a ByteBuffer

Reading from Channel into ByteBuffer

... 
final ByteBuffer buffer = createBuffer(); 
while (fileChannel.read(buffer) != -1) { 
    contents.append(new String(buffer.array())); 
    buffer.clear(); 
} 
... 
private ByteBuffer createBuffer() { 
    return ByteBuffer.allocate(BYTE_BUFFER_LENGTH); 
}
...
  • line 2: A ByteBuffer is created with a defined capacity. (BYTE_BUFFER_LENGTH)
  • line 3: Data is read from the specified FileChannel into the ByteBuffer.
  • line 4: The ByteBuffer’s current contents are added to the StringBuilder. This is done via convenience method array() as a result of the way the ByteBuffer was created in the example (via allocate()).
  • line 5: The ByteBuffer is cleared to prepare for reading more data from the channel, this will set the position cursor back to 0 and allow contents to be read from the FileChannel back into the ByteBuffer repeating the process until no more data is available.

Alternate method for reading from Channel into ByteBuffer 

... 
buffer.flip(); 
if (buffer.hasRemaining()) { 
    byte [] src = new byte[buffer.limit()]; 
    buffer.get(src); 
    contents.append(new String(src)); 
} 
.... 
  • line 2: Invert the position and limit of the Buffer to retrieve what has been read from the Channel.
  • line 3: Ensure there is something to read, ie: The difference between limit and position is > 0.
  • line 4: Create a byte array to be the size of the data in the Buffer.
  • line 5: Retrieve the contents of the Buffer into the byte array.
  • line 6: Create a String array from the contents of the byte array.

It is important to also note that the instantiation of a new String to hold the bytes implicitly uses the default Charset to decode the bytes from their byte values to their corresponding unicode characters. If the default Charset was not what we were looking for, then instantiating a new String with the appropriate Charset would be required.

2.2 Channels

Interface hierarchy of Channel

A Channel is a proxy (open connection proxy) to a component that is responsible for native IO (file or network socket). By acting as a proxy to some native IO component we are able to write and / or read from a Channel. Some Channel implementations allow us to put them into non-blocking mode allowing read and write operations to be non-blocking. The same Channel can be used for both reading and writing.

A Channel is open upon creation and remains that way until it is closed.

2.2.1 Example usage of a FileChannel

Creating a FileChannel

...

final File file = new File(FileChannelReadExample.class.getClassLoader().getResource(path).getFile());
return fileOperation == FileOperation.READ ? new FileInputStream(file).getChannel() : 
                        new FileOutputStream(file).getChannel();
...

  • line 3: Create a File Object
  • line 4: Depending on the type of File operation (read or write) we create the necessary Stream and get the Channel from the Stream.

2.3 Charsets

A Charset is a mapping between 16 bit unicode characters and bytes. Charsets work with decoders and encoders which facilitate the adaption from bytes to characters and vice versa.

  • Encoding: The process of transforming a sequence of characters into bytes
  • Decoding: The process of transforming bytes into character buffers.

Charset provides other utility methods for looking up a Charset by name, creating coders (encoder or decoders) and getting the default Charset. Typically when one works with ByteBuffer and String as is the case in the example, the default Charset is what we would normally use if we do not explicitly specify one. This would suffice most of the time.

Charset usage

...
final Charset defaultCharset = Charset.defaultCharset();
final String text = "Lorem ipsum";
 
final ByteBuffer bufferA = ByteBuffer.wrap(text.getBytes()); 
final ByteBuffer bufferB = defaultCharset.encode(text);
 
final String a = new String(bufferA.array());
final CharBuffer charBufferB = defaultCharset.decode(bufferB);
 
System.out.println(a);
System.out.println(new String(charBufferB.array()));
...
  • line 2: The default Charset is retrieved.
  • line 5: The sample text is wrapped in a ByteBuffer. The default Charset is used implicitly when encoding the characters into bytes.
  • line 6: The sample text is encoded explicitly using the default Charset encoder.
  • line 8: A String is created using the default Charset decoder implicitly .
  • line 9: A Character Buffer (ultimately a String) is created using the default Charset decoder explicitly.

2.4 Selectors

Selectors as the name implies, select from multiple SelectableChannel types and notify our program when IO has happened on one of those channels. It is important to note that during the registration process (registering a SelectableChannel with a Selector) we declare the IO events we are interested in, termed the “interest set” This can be:

  1. Connect
  2. Accept
  3. Read
  4. Write

With this proxy in place and the added benefit of setting those SelectableChannel types into non-blocking mode we are able to multiplex over said channels in a very efficient way, typically with very few threads, even as little as one.

Selector usage with SelectableChannel

try (final Selector selector = Selector.open(); 
     final ServerSocketChannel serverSocket = ServerSocketChannel.open();) {
    final InetSocketAddress hostAddress = 
          new InetSocketAddress(Constants.HOST, Constants.PORT);
    serverSocket.bind(hostAddress);
    serverSocket.configureBlocking(false);
    serverSocket.register(selector, serverSocket.validOps(), null);

    while (true) {
       final int numSelectedKeys = selector.select();
       if (numSelectedKeys > 0) {
           handleSelectionKeys(selector.selectedKeys(), serverSocket);
       }
    }
}

The following code snippet demonstrates iterating through all the SelectionKey instances that indicate IO “ready” events from Channel instances managed by the single Selector. We are only interested in “Accept” and Readable” events. For every new connection accepted an “Accept” event is signaled and we can act on it. Likewise with a “read” ready event we can read incoming data. It is important to remove the  SelectionKey from the set after handling it, as the Selector does not do this and you will continue to process that stale event.

Working with SelectionKeys

final Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
while (selectionKeyIterator.hasNext()) {
    final SelectionKey key = selectionKeyIterator.next();

    if (key.isAcceptable()) {
        acceptClientSocket(key, serverSocket);
    } else if (key.isReadable()) {
        readRequest(key);
    } else {
        System.out.println("Invalid selection key");
    }

    selectionKeyIterator.remove();
}

  • line 13: Remember to remove the SelectionKey from the selected set  as the Selector does not do this for us, if we don’t do it, we will continue to process stale events.

The following code snippet demonstrates the use of  registration of a SocketChannel with the same Selector that manages the ServerSocketChannel. Here, however, the interest set is only for IO “read” events.

Registering a Channel with a Selector

final SocketChannel client = serverSocket.accept();
client.configureBlocking(false);
client.register(key.selector(), SelectionKey.OP_READ);

3. Summary

In this beginners tutorial we understood some of the differences between IO and NIO and reasons for NIO’s existence and applicability. We have also covered the 4 main abstractions when working with NIO. Those are:

  • Buffers
  • Channels
  • Selectors
  • Charsets

We have seen how they can be used and how they work in tandem with each other.  With this tutorial in hand, you understand the basics of creating Channels and using them with Buffers. How to interact with Buffers and the rich API it provides for traversing buffer content. We have also learnt how to register Channels with Selectors and interact with the Selector via its SelectionKey abstraction.

4. Working with example source code

The source code contains the following examples:

  • Charset example.
  • FileChannel example. This example reads from a classpath resource file src/main/resources/file/input.txt and writes a String literal to a classpath resource src/main/resources/file/output.txt. Be sure to check the folder target/classes/file when wanting to view the output of the write example.
  • Client Server example. Start the server first, then start the client. The client will attempt 10 connections to the server and write the same text 10 times to the server which will simply write the contents to console.

5. Download the source code

This was a Java Nio Tutorial for Beginners Example.

Download
You can download the full source code of this example here: Java NIO tutorial for beginners
(No Ratings Yet)
Start the discussion Views Tweet it!

Do you want to know how to develop your skillset to become a Java Rockstar?

Subscribe to our newsletter to start Rocking right now!

To get you started we give you our best selling eBooks for FREE!

 

1. JPA Mini Book

2. JVM Troubleshooting Guide

3. JUnit Tutorial for Unit Testing

4. Java Annotations Tutorial

5. Java Interview Questions

6. Spring Interview Questions

7. Android UI Design

 

and many more ....

 

Receive Java & Developer job alerts in your Area

 

Leave a Reply

avatar
  Subscribe  
Notify of