Java NIOCookbook
Java NIOCookbook
Contents
Preface
java.nio (NIO stands for non-blocking I/O) is a collection of Java programming language APIs that offer features for intensive
I/O operations. It was introduced with the J2SE 1.4 release of Java by Sun Microsystems to complement an existing standard
I/O.
The APIs of NIO were designed to provide access to the low-level I/O operations of modern operating systems. Although the
APIs are themselves relatively high-level, the intent is to facilitate an implementation that can directly use the most efficient
operations of the underlying platform. (Source: https://en.wikipedia.org/wiki/New_I/O_(Java))
In this book, we provide a series of tutorials on Java NIO examples that will help you kick-start your own projects.
Java NIO Programming Cookbook vii
JCGs (Java Code Geeks) is an independent online community focused on creating the ultimate Java to Java developers resource
center; targeted at the technical architect, technical team lead (senior developer), project manager and junior developers alike.
JCGs serve the Java, SOA, Agile and Telecom communities with daily news written by domain experts, articles, tutorials, reviews,
announcements, code snippets and open source projects.
You can find them online at https://www.javacodegeeks.com/
Java NIO Programming Cookbook 1 / 73
Chapter 1
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:
1.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.
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.
Java NIO Programming Cookbook 2 / 73
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.
1.2.1 Buffers
A 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).
• 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).
Java NIO Programming Cookbook 3 / 73
...
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 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.
• 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.
Java NIO Programming Cookbook 4 / 73
1.2.2 Channels
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.
Creating a FileChannel
...
• line 4: Depending on the type of File operation (read or write) we create the necessary Stream and get the Channel from the
Stream.
1.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.
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";
System.out.println(a);
System.out.println(new String(charBufferB.array()));
...
• 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.
1.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:
• Connect
• Accept
• Read
• 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
while (true) {
Java NIO Programming Cookbook 6 / 73
• line 10: We call select() on the Selector which is blocking until some IO occurs on any of the SelectableChannel instances
that are registered with it. It will return the number of keys which are ready for IO activity.
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
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
1.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.
• 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.
Chapter 2
SSL is the secure communication protocol of choice for a large part of the Internet community. There are many applications of
SSL in existence, since it is capable of securing any transmission over TCP. Secure HTTP, or HTTPS, is a familiar application of
SSL in e-commerce or password transactions. Along with this popularity comes demands to use it with different I/O and threading
models in order to satisfy the applications’ performance, scalability, footprint, and other requirements. There are demands to
use it with blocking and non-blocking I/O channels, asynchronous I/O, input and output streams and byte buffers.The main
point of the protocol is to provide privacy and reliability between two communicating applications. The following fundamental
characteristics provide connection security:
Many developers may be wondering how to use SSL with Java NIO. With the traditional blocking sockets API, security is a
simple issue: just set up an SSLContext instance with the appropriate key material, use it to create instances of SSLSocke
tFactory or SSLServerSocketFactory and finally use these factories to create instances of SSLServerSocket or
SSLSocket . In Java 1.6, a new abstraction was introduced to allow applications to use the SSL/TLS protocols in a transport
independent way, and thus freeing applications to choose transport and computing models that best meet their needs. Not only
does this new abstraction allow applications to use non-blocking I/O channels and other I/O models, it also accommodates
different threading models.
The new abstraction is therefore an advanced API having as core class the javax.net.ssl.SSLEngine . It encapsulates
an SSL/TLS state machine and operates on inbound and outbound byte buffers supplied by the user of the SSLEngine.
2.2.1 Lifecycle
The SSLEngine must first go through the handshake, where the server and the client negotiate the cipher suite and the session
keys. This phase typically involves the exchange of several messages. After completing the handshake, the application can
start sending and receiving application data. This is the main state of the engine and will typically last until the connection is
CLOSED (see image below). In some situations, one of the peers may ask for a renegotiation of the session parameters, either
Java NIO Programming Cookbook 9 / 73
to generate new session keys or to change the cipher suite. This forces a re-handshake. When one of the peers is done with the
connection, it should initiate a graceful shutdown, as specified in the SSL/TLS protocol. This involves exchanging a couple of
closure messages between the client and the server to terminate the logical session before physically closing the socket.
Java NIO Programming Cookbook 10 / 73
The two main SSLEngine methods wrap() and unwrap() are responsible for generating and consuming network data re-
spectively. Depending on the state of the SSLEngine, this data might be handshake or application data. Each SSLEngine has
several phases during its lifetime. Before application data can be sent/received, the SSL/TLS protocol requires a handshake
to establish cryptographic parameters. This handshake requires a series of back-and-forth steps by the SSLEngine. The SSL
Process can provide more details about the handshake itself. During the initial handshaking, wrap() and unwrap() generate
and consume handshake data, and the application is responsible for transporting the data. This sequence is repeated until the
handshake is finished. Each SSLEngine operation generates a SSLEngineResult , of which the SSLEngineResult.
HandshakeStatus field is used to determine what operation needs to occur next to move the handshake along. Below is an
example of the handshake process:
The following example creates a connection to https://www.amazon.com/ and displays the decrypted HTTP response.
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.concurrent.Executor;
Java NIO Programming Cookbook 12 / 73
import java.util.concurrent.Executors;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSession;
@Override
public void onSuccess()
{
System.out.println("handshake success");
SSLSession session = engine.getSession();
try
{
System.out.println("local principal: " + session.getLocalPrincipal());
System.out.println("remote principal: " + session.getPeerPrincipal());
System.out.println("cipher: " + session.getCipherSuite());
}
catch (Exception exc)
{
exc.printStackTrace();
}
//HTTP request
StringBuilder http = new StringBuilder();
http.append("GET / HTTP/1.0\\r\\n");
http.append("Connection: close\\r\\n");
http.append("\\r\\n");
byte[] data = http.toString().getBytes();
ByteBuffer send = ByteBuffer.wrap(data);
this.sendAsync(send);
Java NIO Programming Cookbook 13 / 73
@Override
public void onInput(ByteBuffer decrypted)
{
// HTTP response
byte[] dst = new byte[decrypted.remaining()];
decrypted.get(dst);
String response = new String(dst);
System.out.print(response);
System.out.flush();
}
@Override
public void onClosed()
{
System.out.println("ssl session closed");
}
};
// NIO selector
while (true)
{
key.selector().select();
Iterator keys = key.selector().selectedKeys().iterator();
while (keys.hasNext())
{
keys.next();
keys.remove();
ssl.processInput();
}
}
}
}
• In the main() method on lines 18-25, a Selector is created and a SocketChannel is registered having a selection key
interested in socket-connect and socket-read operations for the connection to the amazon url:
• On lines 28-29, an ioWorker thread is created for executing the SSLProvider runnable and also a ThreadPool con-
taining 2 threads for executing the delegated runnable task for the SSL Engine.
• On lines 32-34, the SSLEngine is initiated in client mode and with initial handshaking:
• On lines 36-59, the NioSSLProvider object is instantiated. This is responsible for writing and reading from the ByteCha
nnel and also as the entry point for the SSL Handshaking. Upon successful negotiation with the amazon server, the local and
remote principals are printed and also the name of the SSL cipher suite which is used for all connections in the session.
Java NIO Programming Cookbook 14 / 73
• The HTTP request is sent from the client after successful handshake on lines 62-67:
• On line 72, the onInput method is called whenever the SSL Engine completed an operation with javax.net.ssl.
SSLEngineResult.Status.OK . The partial decrypted response is printed each time:
• Finally, the nio Selector loop is started on line 90 by processing the selection keys which remain valid until the channel is
closed.
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.WritableByteChannel;
import java.util.concurrent.Executor;
import javax.net.ssl.SSLEngine;
@Override
public void onOutput(ByteBuffer encrypted)
{
try
{
((WritableByteChannel) this.key.channel()).write(encrypted);
}
catch (IOException exc)
{
Java NIO Programming Cookbook 15 / 73
• The notify method is called on line 53, which triggers the ssl handshake procedure and via the helper method isHandSha
king on line 1 of the SSLProvider class, the wrap/unwrap sequence starts.
• If the wrap() helper method from the SSLProvider class is called, then the buffered data are encoded into SSL/TLS network
data:
and if the return value of the SSLEngine operation is OK then the onOutput() method on line 22 is called in order to write
the encrypted response from the server into the ByteChannel :
((WritableByteChannel) this.key.channel()).write(encrypted);
• If the unwrap() helper method from the SSLProvider class is called, then an attempt to decode the SSL network data from
the server is made on line 95 of the SSLProvider class:
and if the return value of the SSLEngine operation is OK, the decrypted message from the server is printed.
Java NIO Programming Cookbook 16 / 73
case NEED_WRAP:
if (!this.wrap())
return false;
break;
case NEED_UNWRAP:
if (!this.unwrap())
return false;
break;
case NEED_TASK:
final Runnable sslTask = engine.getDelegatedTask();
Runnable wrappedTask = new Runnable()
{
@Override
public void run()
{
sslTask.run();
ioWorker.execute(SSLProvider.this);
}
};
taskWorkers.execute(wrappedTask);
return false;
case FINISHED:
throw new IllegalStateException("FINISHED");
}
return true;
}
try
{
clientWrap.flip();
wrapResult = engine.wrap(clientWrap, serverWrap);
clientWrap.compact();
}
catch (SSLException exc)
{
this.onFailure(exc);
Java NIO Programming Cookbook 17 / 73
return false;
}
switch (wrapResult.getStatus())
{
case OK:
if (serverWrap.position() > 0)
{
serverWrap.flip();
this.onOutput(serverWrap);
serverWrap.compact();
}
break;
case BUFFER_UNDERFLOW:
// try again later
break;
case BUFFER_OVERFLOW:
throw new IllegalStateException("failed to wrap");
case CLOSED:
this.onClosed();
return false;
}
return true;
}
try
{
clientUnwrap.flip();
unwrapResult = engine.unwrap(clientUnwrap, serverUnwrap);
clientUnwrap.compact();
}
catch (SSLException ex)
{
this.onFailure(ex);
return false;
}
switch (unwrapResult.getStatus())
{
case OK:
if (serverUnwrap.position() > 0)
{
serverUnwrap.flip();
this.onInput(serverUnwrap);
serverUnwrap.compact();
}
break;
case CLOSED:
this.onClosed();
return false;
case BUFFER_OVERFLOW:
throw new IllegalStateException("failed to unwrap");
Java NIO Programming Cookbook 18 / 73
case BUFFER_UNDERFLOW:
return false;
}
if (unwrapResult.getHandshakeStatus() == HandshakeStatus.FINISHED)
{
this.onSuccess();
return false;
}
return true;
}
Chapter 3
This article introduces the SocketChannel class and its basic usage. This class is defined in the java.nio package.
Socket programming involves two systems communicating with one another. In implementations prior to NIO, Java TCP client
socket code is handled by the java.net.Socket class. A socket is one end-point of a two-way communication link between two
programs running on the network. Socket classes are used to represent the connection between a client program and a server
program. The java.net package provides two classes, Socket and ServerSocket , that implement the client side of the
connection and the server side of the connection, respectively. The below image illustrates the nature of this communication:
A socket is basically a blocking input/output device. It makes the thread that is using it to block on reads and potentially also
block on writes if the underlying buffer is full. Therefore, different threads are required if the server has many open sockets.
From a simplistic perspective, the process of a blocking socket communication is as follows:
• Invoke the ServerSocket’s accept() method to listen on the configured port for a client connection.
• When a client connects to the server, the accept() method returns a Socket through which the server can communicate
with the client: an InputStream is obtained to read from the client and an OutputStream to write to the client.
With the standard java sockets, if the server needed to be scalable, the socket had to be passed to another thread for processing
so that the server could continue listening for additional connections, meaning call the ServerSocket’s accept() method again
to listen for another connection.
A SocketChannel on the other hand is a non-blocking way to read from sockets, so that you can have one thread communicate
with multiple open connections at once. With socket channel we describe the communication channel between client and server.
It is identified by the server IP address and the port number. Data passes through the socket channel by buffer items. A selector
monitors the recorded socket channels and serializes the requests, which the server has to satisfy. The Keys describe the objects
used by the selector to sort the requests. Each key represents a single client sub-request and contains information to identify
the client and the type of the request. With non-blocking I/O, someone can program networked applications to handle multiple
simultaneous connections without having to manage multiple thread collection, while also taking advantage of the new server
scalability that is built in to java.nio. The below image illustrates this procedure:
3.3 Example
The following example shows the use of SocketChannel for creating a simple echo server, meaning it echoes back any
message it receives.
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
Java NIO Programming Cookbook 21 / 73
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.*;
}
};
}
};
new Thread(server).start();
new Thread(client, "client-A").start();
new Thread(client, "client-B").start();
}
System.out.println("Server started...");
while (true) {
// wait for events
this.selector.select();
Java NIO Programming Cookbook 22 / 73
if (!key.isValid()) {
continue;
}
if (key.isAcceptable()) {
this.accept(key);
}
else if (key.isReadable()) {
this.read(key);
}
}
}
}
if (numRead == -1) {
this.dataMapper.remove(channel);
Socket socket = channel.socket();
SocketAddress remoteAddr = socket.getRemoteSocketAddress();
System.out.println("Connection closed by client: " + remoteAddr);
channel.close();
key.cancel();
return;
}
• In the main() method on lines 43-45, one thread for creating the ServerSocketChannel is started and two client threads
responsible for starting the clients which will create a SocketChannel for sending messsages to the server.
new Thread(server).start();
new Thread(client, "client-A").start();
new Thread(client, "client-B").start();
• In the startServer() method on line 54, the server SocketChannel is created as nonBlocking, the server socket is
retrieved and bound to the specified port:
Finally, the register method associates the selector to the socket channel.
serverChannel.register(this.selector, SelectionKey.OP_ACCEPT);
The second parameter represents the type of the registration. In this case, we use OP_ACCEPT , which means the selector merely
reports that a client attempts a connection to the server. Other possible options are: OP_CONNECT , which will be used by the
client; OP_READ ; and OP_WRITE . After that, the select method is used on line 67, which blocks the execution and waits
for events recorded on the selector in an infinite loop.
this.selector.select();
• The selector waits for events and creates the keys. According to the key-types, an opportune operation is performed. There are
four possible types for a key:
• If an acceptable key is found, the accept(SelectionKey key) on line 93 is invoked in order to create a channel which
accepts this connection, creates a standard java socket on line 97 and register the channel with the selector:
• After receiving a readable key from the client, the read(SelectionKey key) is called on line 107 which reads from the
socket channel. A byte buffer is allocated for reading from the channel
numRead = channel.read(buffer);
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
System.out.println("Client... started");
• In the above client code, each client thread creates a socket channel on the server’s host address on line 12:
• On line 19, a String array is created to be transmitted to the server using the previously created socket. The data contain also
each thread’s name for distinguishing the sender:
and each message is written to the channel from the given buffer on line 25:
ByteBuffer buffer = ByteBuffer.wrap(message);
Java NIO Programming Cookbook 25 / 73
Server started...
Client... started
Client... started
client-A: test1
client-B: test1
Connected to: /127.0.0.1:51468
Got: client-B: test1
Connected to: /127.0.0.1:51467
Got: client-A: test1
client-A: test2
client-B: test2
Got: client-B: test2
Got: client-A: test2
client-A: test3
client-B: test3
Got: client-B: test3
Got: client-A: test3
Connection closed by client: /127.0.0.1:51468
Connection closed by client: /127.0.0.1:51467
Chapter 4
With this example we are going to demonstrate how to use the Non-blocking I/O API, or NIO.2 API (NIO API) for short, to
write data to a file. The examples in this article are compiled and run in a Mac OS unix environment.
Please note that Java SE 8 is required to run the code in this article.
The NIO.2 API was introduced in Java 7 as a replacement for the java.io.File class. It provides a flexible, and intuitive
API for use with files.
In order to write a file to the file system we must first create a Path for the destination of the file. A Path object is a hierarchical
representation of the path on a system to the file or directory. The java.nio.file.Path interface is the primary entry point
for working with the NIO 2 API.
The easiest way to create a Path Object is to use the java.nio.files.Paths factory class. The class has a static get()
method which can be used to obtain a reference to a file or directory. The method accepts either a string, or a sequence of
strings(which it will join to form a path) as parameters. A java.nio.file.Path , like a File , may refer to either an
absolute or relative path within the file system. This is displayed in the following examples:
Path p1 = Paths.get( "cats/fluffy.jpg" );
Path p2 = Paths.get("/home/project");
Path p3 = Paths.get("/animals", "dogs", "labradors");
In the above:
Once we have a Path Object we are able to execute most of the operations that were previously possible with java.io.File .
Java NIO Programming Cookbook 27 / 73
The Files.write() method is an option when writing strings to file. It is cleanly and concisely expressed. It is not a good
idea to use a method like this to write larger files, due to its reliance on byte arrays. As shown below there are more efficient
ways of handling these.
Path path = Paths.get("src/main/resources/question.txt");
Files.write(path, question.getBytes());
The NIO.2 API has methods for writing files using java.io streams. The Files.newBufferedWriter(Path,Charset) writes to the file
at the specified Path location, using the user defined Charset for character encoding. This BufferedWriter method is preferential
due to its efficient performance especially when completing a large amount of write operations. Buffered operations have this
effect as they aren’t required to call the operating system’s write method for every single byte, reducing on costly I/O operations.
Here’s an example:
Path path = Paths.get("src/main/resources/shakespeare.txt");
try(BufferedWriter writer = Files.newBufferedWriter(path, Charset.forName("UTF-8"))){
writer.write("To be, or not to be. That is the question.");
}catch(IOException ex){
ex.printStackTrace();
}
This method will create a new file at the given path, or overwrite it if it already exists.
In this example we use the NIO API in conjunction with an output stream to copy a file and its contents from one file to a new
instance with a new name. We achieve this by using the Files.copy() method, which accepts (Path source, Outputstream os) as
its parameters.
Path oldFile = Paths.get("src/main/resources/", "oldFile.txt");
Path newFile = Paths.get("src/main/resources/", "newFile.txt");
try (OutputStream os = new FileOutputStream(newFile.toFile())) {
Files.copy(oldFile, os);
The code above is an example of the try with resources syntax which was introduced in Java 7 and made it easier handle
exceptions while correctly closing streams and other resources which are used in a try-catch block. FileOutputStream is used to
handle binary data.
4.4 Summary
In this article we’ve introduced you a couple ways to use the NIO API to write a file to your file system. Along the way we’ve
touched upon the Path object.
Java NIO Programming Cookbook 28 / 73
Chapter 5
This article is a tutorial on implementing a simple Java NIO Heartbeat. This example will take the form of "n" number of
"Broadcast" mode processes which will multicast data via UDP to "n" number of "Subscribe" processes that have expressed
interest in receiving said traffic.
5.1 Introduction
This article builds on three earlier articles on the subject of Java NIO, namely Java Nio Tutorial for Beginners, Java Nio Asyn-
chronous Channels Tutorial and "Java Nio EchoServer". Before getting stuck into the "meat" of our example, it is best to get
some background into the topic itself. According to Wikipedia a "heartbeat" in computer systems is a periodic signal generated
by hardware or software to indicate normal operation or to synchronize parts of a system. So true to the name it is indeed a
measure of life of individual components in a distributed computer system and one can deduce then by it’s absence, presence and
frequency, the state of the system in which it occurs.
In the same breath, when one talks about "heartbeats" in computer systems the term "UDP" often comes up and with good reason.
It is the protocol of choice when implementing "hearbeat" type solutions, weather it be cluster membership negotiations or life
signing (heartbeats). The low latency of this "connection-less" protocol also plays to the nature of "heartbeating" in distributed
systems.
Important to note that unlike TCP, UDP makes no guarantee on delivery of packets, the low latency of UDP stems from this not
having to guarantee delivery via the typical SYN ACK (3 way handshake etc).
We go one step further in this example and we multicast the traffic out to interested parties. Now why would we do this and what
other choices are there? Typically the following choices would present themselves:
• Broadcast: From one machine to all possible machines. One-to-All (within the broadcast domain - ie: behind a router or in a
private network)
• Multicast: From one machine to multiple machines that have stated interest in receiving said traffic. This can traverse the
broadcast domain and extend past a router.
Java NIO Programming Cookbook 30 / 73
The example code in this article was built and run using:
5.3 Overview
The abstractions of use to us when wanting to effect UDP in Java Nio would be the DatagramChannel, which also happens
to be a SelectableChannel priming it for use by a Selector in a very Thread efficient manner. It also happens to implement
MulticastChannel which supports Internet Protocol (IP) multicasting.
Java NIO Programming Cookbook 31 / 73
5.3.1 DatagramChannel
A DatagramChannel is opened by one of the static open(...) methods of the class itself. One of the open(...) methods
are of particular interest to us and that is:
DatagramChannel open for multicast
The ProtocolFamily is required when attempting to multicast with this Channel and should correspond to the IP type of the
multicast group that this Channel will join. eg: IPV4 StandardProtocolFamily.INET A DatagramChannel need not be
connected to use the send(...) and receive(...) methods of this class, conversely so the read(...) and write(.
..) methods do.
5.3.2 MulticastChannel
This Channel supports (IP) multicasting. Of particular interest to us is this part of it’s API:
DatagramChannel configuration
...
channel.setOption(StandardSocketOptions.IP_MULTICAST_IF, NetworkInterface);
channel.join(InetAddress, this.multicastNetworkInterface);
...
line 2: the NetworkInterface is the interface through which we will send / receive UDP multicast traffic line 3: we ensure we join
the multicast group (express interest in receiving traffic to this group) by way of passing a InetAddress (the multicast IP) and a
NetworkInterface (the interface through which we will receive said multicast traffic). Multicast IP ranges range from 224.0.0.0
to 239.255.255.255 typically.
A MulticastChannel can join "n" number of multicast groups and can join a group on different network interfaces.
5.4 Multicaster
Multicaster
Multicaster(final String id, final String ip, final String interfaceName, final int ←-
port, final int poolSize) {
if (StringUtils.isEmpty(id) || StringUtils.isEmpty(ip) || StringUtils.isEmpty( ←-
interfaceName)) {
throw new IllegalArgumentException("required id, ip and interfaceName");
}
this.id = id;
this.scheduler = Executors.newScheduledThreadPool(poolSize);
this.multicastGroup = new InetSocketAddress(ip, port);
try {
this.networkInterface = NetworkInterface.getByName(interfaceName);
} catch (SocketException e) {
Java NIO Programming Cookbook 32 / 73
@Override
public ScheduledExecutorService getService() {
return this.scheduler;
}
initChannel(channel);
doSchedule(channel);
endLatch.await();
} catch (IOException | InterruptedException e) {
throw new RuntimeException("unable to run broadcaster", e);
} finally {
this.scheduler.shutdownNow();
}
}
try {
Multicaster.this.doBroadcast(channel);
} catch (IOException e) {
e.printStackTrace();
}
}
}, 0L, Constants.Schedule.PULSE_DELAY_IN_MILLISECONDS, TimeUnit.MILLISECONDS);
}
channel.bind(null);
channel.setOption(StandardSocketOptions.IP_MULTICAST_IF, this.networkInterface);
}
• line 14: we create a ScheduledExecutorService with the purposes of scheduling the multicast heartbeat pulse to the multicast
group
• line 15: we create a InetSocketAddress which will be the multicast group to which we will send our heartbeats
• line 18: we create a NetworkInterface which will encapsulate the interface through which our multicast heartbeats will travel
Java NIO Programming Cookbook 33 / 73
5.5 Subscriber
Subscriber
Subscriber(final String id, final String ip, final String interfaceName, final int port ←-
, final int poolSize) {
if (StringUtils.isEmpty(id) && StringUtils.isEmpty(ip) || StringUtils.isEmpty( ←-
interfaceName)) {
throw new IllegalArgumentException("required id, ip and interfaceName");
}
this.id = id;
this.scheduler = Executors.newScheduledThreadPool(poolSize);
this.hostAddress = new InetSocketAddress(port);
this.pulses = new ConcurrentHashMap<>();
try {
this.networkInterface = NetworkInterface.getByName(interfaceName);
this.group = InetAddress.getByName(ip);
} catch (SocketException | UnknownHostException e) {
throw new RuntimeException("unable to start broadcaster", e);
}
}
@Override
public ScheduledExecutorService getService() {
return this.scheduler;
}
void run() {
try (final DatagramChannel channel = DatagramChannel.open(StandardProtocolFamily. ←-
INET); final Selector selector = Selector.open()) {
while (!Thread.currentThread().isInterrupted()) {
if (selector.isOpen()) {
Java NIO Programming Cookbook 34 / 73
channel.configureBlocking(false);
channel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
channel.bind(this.hostAddress);
channel.setOption(StandardSocketOptions.IP_MULTICAST_IF, this.networkInterface);
channel.join(this.group, this.networkInterface);
channel.register(selector, SelectionKey.OP_READ);
}
} else {
System.out.println(String.format("OK : %s is up", id));
}
});
}
}, 0L, Constants.Schedule.PULSE_DELAY_IN_MILLISECONDS, TimeUnit.MILLISECONDS);
}
}
• line 16: we create a ScheduledExecutorService to schedule the polling of the heartbeat pulses we have received thus far via
UDP multicast
• line 17: we create a InetSocketAddress for the specified port and instantiate it for the "localhost"
• line 21: we create NetworkInterface for the specified interface name, this will be the interface through which the Subscriber
will receive UDP multicast heartbeat pulses
• line 22: we create a InetAddress representing the multicast group from which we will receive multicast messages
• line 34: we open the DatagramChannel but also specify the ProtocolFamily and this should correspond to the address
type of the multicast group this Channel will be joining.
• line 37-38: we initialize the Channel and schedule the polling of heartbeat pulses
• line 40-49: while the current thread is still running we utilize the Selector and await incoming UDP multicast heartbeats in
a non-blocking way.
• line 63-64: we set the multicast interface and join the multicast group using the multicast interface
• line 77-79: we read a Pulse from the UDP multicast packet.
The example project is a maven project and must be built into a "fat" or "uber" jar by issuing the following command mvn
clean install package . The resulting artifact can be found in the "target" folder located in the project root folder. The
project can be run in two modes, one being "MULTICAST" and the other being "SUBSCRIBE". Obviously the "MULTICAST"
mode will publish packets (heartbeats) to the multicast group and the "SUBSCRIBE" mode will receive said heartbeats.
The beauty of the example is that you can spin up as many "MULTICAST" processes as you wish (ensure you give them all
unique id’s) and as many "SUBSCRIBE" processes as you wish (ensure you give them also unique id’s). This can be done in
random order meaning "MULTICAST" or "SUBSCRIBE" in any order. Simply put, as soon as heartbeats arrive, the subscribers
will know about it and begin reporting as shown below:
Java NIO Programming Cookbook 36 / 73
On top are the two "MULTICAST" processes and on the bottom are the two "SUBSCRIBE" processes. Notice how the sub-
scribers report the other processes being "up" until I terminate one of them ("A" - top left) and then after a configurable tolerance
is exceeded, the last packet / pulse is reported as too old, the subscribers notify us that "A is down". After a short while A is
removed and considered dead. We then bring A back up and you see immediately a heartbeat is received and the subscribers
begin reporting "A is up" along with "B", which never went down.
To run this example you need a network interface, ie: you need to be plugged into a network point as the network interface
name is required. Although I have been successful in sending packets via the wireless interface as the "BROADCASTER", to
the multicast group, and receiving them in the "SUBSCRIBER" via a LAN interface, I would highly recommend using a LAN
(cable) interface for both MULTICAST and BROADCAST processes.The interface will serve as the NetworkInterface on which
the multicast group is joined.
The following arguments are required to run the program:
5.7 Summary
In this example we demonstrated how to build a simple heartbeat application using UDP and Java NIO. We took it a bit further
and leveraged multicasting as a method to publish our heartbeat signals across the network to interested parties. These interested
Java NIO Programming Cookbook 37 / 73
parties can change state, notify persons or even try to resurrect down / faulty services when they become aware of problems via
the heartbeat signals.
Chapter 6
This article is a tutorial on transferring a large file using Java Nio. It will take shape via two examples demonstrating a simple
local file transfer from one location on hard disk to another and then via sockets from one remote location to another remote
location.
6.1 Introduction
This tutorial will make use of the FileChannel abstraction for both remote and local copy. Augmenting the remote copy process
will be a simple set of abstractions (ServerSocketChannel & SocketChannel) that facilitate the transfer of bytes over the wire.
Finally we wrap things up with an asynchronous implementation of large file transfer. The tutorial will be driven by unit tests
that can run from command line using maven or from within your IDE.
The example code in this article was built and run using:
6.3 FileChannel
A FileChannel is a type of Channel used for writing, reading, mapping and manipulating a File. In addition to the familiar
Channel (read, write and close) operations, this Channel has a few specific operations:
• Has the concept of an absolute position in the File which does not affect the Channels current position.
• Parts or regions of a File can be mapped directly into memory and work from memory, very useful when dealing with large
files.
• Writes can be forced to the underlying storage device, ensuring write persistence.
• Bytes can be transferred from one ReadableByteChannel / WritableByteChannel instance to another ReadableByteChannel /
WritableByteChannel, which FileChannel implements. This yields tremendous IO performance advantages that some Operat-
ing systems are optimized for.
Java NIO Programming Cookbook 39 / 73
• A part or region of a File may be locked by a process to guard against access by other processes.
FileChannels are thread safe. Only one IO operation that involves the FileChannels position can be in flight at any given point in
time, blocking others. The view or snapshot of a File via a FileChannel is consistent with other views of the same File within the
same process. However, the same cannot be said for other processes. A file channel can be created in the following ways:
• . . . FileChannel.open(...)
• . . . FileInputStream(...).getChannel()
• . . . FileOutputStream(...).getChannel()
• . . . RandomAccessFile(...).getChannel()
Using one of the stream interfaces to obtain a FileChannel will yield a Channel that allows either read, write or append privileges
and this is directly attributed to the type of Stream (FileInputStream or FileOutputStream) that was used to get the Channel.
Append mode is a configuration artifact of a FileOutputStream constructor.
6.4 Background
The sample program for this example will demonstrate the following:
Particularly with large files the advantages of asynchronous non blocking handling of file transfer cannot be stressed enough.
Large files tying up connection handling threads soon starve a server of resources to handle additional requests possibly for more
large file transfers.
6.5 Program
The code sample can be split into local and remote domains and within remote we further specialize an asynchronous implemen-
tation of file transfer, at least on the receipt side which is arguably the more interesting part.
FileCopy
private FileCop() {
throw new IllegalStateException(Constants.INSTANTIATION_NOT_ALLOWED);
}
public static void copy(final String src, final String target) throws IOException {
if (StringUtils.isEmpty(src) || StringUtils.isEmpty(target)) {
throw new IllegalArgumentException("src and target required");
}
private static void transfer(final FileChannel from, final FileChannel to, long ←-
position, long size) throws IOException {
assert !Objects.isNull(from) && !Objects.isNull(to);
• line 14: we open the from Channel with the StandardOpenOption.READ meaning that this Channel will only be read
from. The path is provided.
• line 15: the to Channel is opened with the intention to write and create, the path is provided.
• line 31-37: the two Channels are provided (from & to) along with the position (initially where to start reading from) and
the size indicating the amount of bytes to transfer in total. A loop is started where attempts are made to transfer up to
Constants.TRANSFER_MAX_SIZE in bytes from the from Channel to the to Channel. After each iteration the amount
of bytes transferred is added to the position which then advances the cursor for the next transfer attempt.
FileReader
this.sender = sender;
this.channel = FileChannel.open(Paths.get(path), StandardOpenOption.READ);
}
transfer();
} finally {
close();
}
}
• line 12: the FileChannel is opened with the intent to read StandardOpenOption.READ , the path is provided to the File.
• line 15-21: we ensure we transfer the contents of the FileChannel entirely and the close the Channel.
• line 23-26: we close the sender resources and then close the FileChannel
• line 29: we call transfer(...) on the sender to transfer all the bytes from the FileChannel
FileSender
final class FileSender {
void transfer(final FileChannel channel, long position, long size) throws IOException {
assert !Objects.isNull(channel);
SocketChannel getChannel() {
return this.client;
}
line 11-17: we provide the FileChannel, position and size of the bytes to transfer from the given channel . A loop is
started where attempts are made to transfer up to Constants.TRANSFER_MAX_SIZE in bytes from the provided Channel
to the SocketChannel client . After each iteration the amount of bytes transferred is added to the position which then
advances the cursor for the next transfer attempt.
FileReceiver
Java NIO Programming Cookbook 42 / 73
channel = serverSocketChannel.accept();
doTransfer(channel);
} finally {
if (!Objects.isNull(channel)) {
channel.close();
}
this.fileWriter.close();
}
}
this.fileWriter.transfer(channel, this.size);
}
serverSocketChannel.bind(new InetSocketAddress(this.port));
}
}
The FileReceiver is a mini server that listens for incoming connections on the localhost and upon connection, accepts
it and initiates a transfer of bytes from the accepted Channel via the FileWriter abstraction to the encapsulated FileChannel
within the FileWriter . The FileReceiver is only responsible for receiving the bytes via socket and then delegates
transferring them to the FileWriter .
FileWriter
int bytesWritten = 0;
while(buffer.hasRemaining()) {
bytesWritten += this.channel.write(buffer, position + bytesWritten);
}
return bytesWritten;
}
The FileWriter is simply charged with transferring the bytes from a SocketChannel to it’s encapsulated FileChannel. As
before, the transfer process is a loop which attempts to transfer up to Constants.TRANSFER_MAX_SIZE bytes with each
iteration.
The following code snippets demonstrate transferring a large file from one remote location to another via an asynchronous
receiver FileReceiverAsync .
OnComplete
@FunctionalInterface
public interface OnComplete {
The OnComplete interface represents a callback abstraction that we pass to our FileReceiverAsync implementation with
the purposes of executing this once a file has been successfully and thoroughly transferred. We pass a FileWriterProxy to
the onComplete(...) and this can server as context when executing said method.
FileWriterProxy
String getFileName() {
return this.fileName;
}
FileWriter getFileWriter() {
return this.fileWriter;
}
AtomicLong getPosition() {
return this.position;
}
boolean done() {
return this.position.get() == this.size;
}
}
The FileWriterProxy represents a proxy abstraction that wraps a FileWriter and encapsulates FileMetaData . All
of this is needed when determining what to name the file, where to write the file and what the file size is so that we know when
the file transfer is complete. During transfer negotiation this meta information is compiled via a custom protocol we implement
before actual file transfer takes place.
FileReceiverAsync
FileReceiverAsync(final int port, final int poolSize, final String path, final ←-
OnComplete onFileComplete) {
assert !Objects.isNull(path);
this.path = path;
this.onFileComplete = onFileComplete;
try {
this.group = AsynchronousChannelGroup.withThreadPool(Executors. ←-
newFixedThreadPool(poolSize));
this.server = AsynchronousServerSocketChannel.open(this.group).bind(new ←-
InetSocketAddress(port));
} catch (IOException e) {
throw new IllegalStateException("unable to start FileReceiver", e);
}
}
void start() {
accept();
}
try {
this.group.shutdown();
this.group.awaitTermination(wait, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
throw new RuntimeException("unable to stop FileReceiver", e);
}
}
@Override
public void completed(final Integer result, final FileWriterProxy attachment) {
if (result >= 0) {
if (result > 0) {
writeToFile(channel, buffer, attachment);
}
buffer.clear();
channel.read(buffer, attachment, this);
} else if (result < 0 || attachment.done()) {
onComplete(attachment);
close(channel, attachment);
}
}
@Override
public void failed(final Throwable exc, final FileWriterProxy attachment) {
throw new RuntimeException("unable to read data", exc);
}
});
}
this.onFileComplete.onComplete(proxy);
}
@Override
public void completed(final Integer result, final StringBuffer attachment) {
if (result < 0) {
close(channel, null);
} else {
if (result > 0) {
attachment.append(new String(buffer.array()).trim());
}
if (attachment.toString().contains(Constants.END_MESSAGE_MARKER)) {
Java NIO Programming Cookbook 46 / 73
try {
fileWriterProxy = new FileWriterProxy(FileReceiverAsync.this. ←-
path, metaData);
confirm(channel, fileWriterProxy);
} catch (IOException e) {
close(channel, null);
throw new RuntimeException("unable to create file writer proxy" ←-
, e);
}
} else {
buffer.clear();
channel.read(buffer, attachment, this);
}
}
}
@Override
public void failed(final Throwable exc, final StringBuffer attachment) {
close(channel, null);
throw new RuntimeException("unable to read meta data", exc);
}
});
}
@Override
public void completed(final Integer result, final Void attachment) {
while (buffer.hasRemaining()) {
channel.write(buffer, null, this);
}
read(channel, proxy);
}
@Override
public void failed(final Throwable exc, final Void attachment) {
close(channel, null);
throw new RuntimeException("unable to confirm", exc);
}
});
}
meta(channel);
}
try {
buffer.flip();
try {
if (!Objects.isNull(proxy)) {
proxy.getFileWriter().close();
}
channel.close();
} catch (IOException e) {
throw new RuntimeException("unable to close channel and FileWriter", e);
}
}
The FileReceiverAsync abstraction builds upon the idiomatic use of AsynchronousChannels demonstrated in this tutorial.
The program can be run from within the IDE, using the normal JUnit Runner or from the command line using maven. Ensure
that the test resources (large source files and target directories exist). Running tests from command line
mvn clean install
You can edit these in the AbstractTest and FileCopyAsyncTest classes. Fair warning the FileCopyAsyncTest can
run for a while as it is designed to copy two large files asynchronously, and the test case waits on a CountDownLatch without a
max wait time specified.
I ran the tests using the "spring-tool-suite-3.8.1.RELEASE-e4.6-linux-gtk-x86_64.tar.gz" file downloaded from the SpringSource
website. This file is approximately 483mb large and below are my test elapsed times. (using a very old laptop).
Test elapsed time
Java NIO Programming Cookbook 48 / 73
Running com.javacodegeeks.nio.large_file_transfer.remote.FileCopyTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.459 sec - in com. ←-
javacodegeeks.nio.large_file_transfer.remote.FileCopyTest
Running com.javacodegeeks.nio.large_file_transfer.remote.FileCopyAsyncTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 26.423 sec - in com. ←-
javacodegeeks.nio.large_file_transfer.remote.FileCopyAsyncTest
Running com.javacodegeeks.nio.large_file_transfer.local.FileCopyTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.562 sec - in com. ←-
javacodegeeks.nio.large_file_transfer.local.FileCopyTest
6.7 Summary
In this tutorial, we demonstrated how to transfer a large file from one point to another. This was showcased via a local copy and
a remote transfer via sockets. We went one step further and demonstrated transferring a large file from one remote location to
another via an asynchronous receiving node.
Chapter 7
This article is a tutorial on the Asynchronous Channels API which was released as part of Java 7. The API can be viewed here.
The example code will demonstrate use of the core abstractions of this API and will capture the essence of using the API.
7.1 Introduction
The Asynchronous Channels API’s supplemented the core Java NIO API’s with additional functionality in the Java 7 release.
Coined NIO.2 the supplement provided many utilities for NIO usage but the crown jewel was the AsynchronousChannel API’s.
A common phrase thrown around when discussing Java NIO is "non-blocking" but now one gets to add the word "asynchronous"
as well. This can lead to a wonderful ice breaker in the form of "non-blocking asynchronous IO".
What a mouthful and even I had difficulty thoroughly digesting and understanding that, but I feel it important to understand what
that phrase means and how it relates to the AsynchronousChannel API’s.
• Asynchronous IO is where an interface or API allows us to provide call back code, to be executed when a particular IO
operation completes. This is where the AsynchronousChannel class and much of it’s hierarchy come into play.
• Non blocking IO is where an IO operation will return immediately either with data, an error or no data. ie: When reading from
a non-blocking channel, either the number of bytes read is returned or -1 meaning nothing more to read or an exception is
thrown if some invalid state is encountered. Java NIO in JDK 1.4 introduced us to the Selector which was an abstraction that
allowed us to leverage non-blocking IO.
AsynchronousChannel instances proxy IO operations and provide a means for notifying the program when said operations com-
plete.
Java NIO Programming Cookbook 50 / 73
The example code in this article was built and run using:
When interacting (reading, writing or connecting) with the AsynchronousChannel API the results of these interactions result in
"Future" results or "Complete" results.
• Future results are encapsulated in the Future API. This facilitates a "pending" result which can later be retrieved or acted on by
leveraging the Future API.
• Complete results are "hooked" into by supplying a CompletionHandler implementation to the method call (read, write or
connect).
7.4 AsynchronousChannel
The AsynchronousChannel is a specialization of the Channel interface that enhances IO operations (read, write, connect or close)
with asynchronous abilities. Calling read() or write() or connect() on the AsynchronousChannel produces a different
result and provides a different method signature to that of the conventional NIO Channel implementations. This varies by way
of:
• Allowing a CompletionHandler implementation to be injected at method invocation to facilitate call back style processing
when the IO event completes normally or via error.
• All methods being asynchronous return immediately and delegate processing of the IO operation to the kernel, with the in-
struction of being notified when the IO operation completes, either by way of the CompletionHandler implementation being
invoked or the Future getting it’s result.
Calling close() simply closes the Channel asynchronously and ensures any outstanding IO operations terminate via an Asyn-
chronousCloseException. Typically AsynchronousChannel implementations are associated with an explicit Thread pool by way
of the AsynchronousChannelGroup implementation which effectively manages all Channel instances associated with it and
provides Thread resources for all Channel instances it manages to handle their IO operations. An AsynchronousChannel imple-
mentation is associated with the AsynchronousChannelGroup at construction time via the following:
• AsynchronousSocketChannel: AsynchronousSocketChannel.open(group)
• AsynchronousServerSocketChannel: AsynchronousServerSocketChannel.open(group)
What follows now are simple snippets of CompletionHandler and Future usage of the AsynchronousChannel API.
CompletionHandler example
Java NIO Programming Cookbook 51 / 73
@Override
public void failed(final Throwable exc, final Object attachment) {...
}
});
• line 1: this method is called with a Buffer implementation and a position in the file to write from. The implementation will
start writing from the given position and continue writing bytes until the buffer is written out to file. The Future return
value encapsulates the pending result of how many bytes were written to the file.
7.5 AsynchronousByteChannel
The AsynchronousByteChannel is a specialization of the AsynchronousChannel that reads and write bytes. It is implemented
concretely by AsynchronousSocketChannel.
7.6 AsynchronousFileChannel
The AsynchronousFileChannel class is an asynchronous channel for reading, writing, and manipulating a file via ByteBuffers.
Creating an AsynchronousFileChannel instance can be done via the two static open(...) methods:
AsynchronousFileChannel open method#1
OpenOption, more specifically StandardOpenOption enumerates the various modes / options the File is manipulated with, eg:
OPEN, READ, WRITE etc and will naturally have an effect on what can be done with the file. Interestingly enough the
Channel does not allow for an AsynchronousChannelGroup at construction but rather an ExecutorService to allow for explicit
thread resource usage as opposed to a default thread group.
The AsynchronousFileChannel provides methods for locking files, truncating files, and retrieving file sizes. Read and write
actions expect a ByteBuffer and a position, position being the location in the file to start reading or writing from, illustrating one
of the main differences between the FileChannel class. The position being required for multithreaded use. This type of Channel
is safe for multithreaded use and multiple IO (read and write) operations can be outstanding at the same time but their order of
execution is undetermined, be aware of this!
FileLocks, another feature of AsynchronousFileChannels, are as the name implies but can vary by type of lock and operating
system support.
• shared lock - meaning the lock can be shared provided the lock granularity is "shared". Also the Channel has to be opened in
READ mode otherwise a NonReadableChannelException will be thrown.
• exclusive lock - only one lock is held. Also the Channel must be opened in write mode otherwise a NonWritableChannelEx-
ception will be thrown.
FileLocks can also lock the entire file or regions of the file based on position. eg: Locking a file from position 10 would imply
locking the file from the 10th byte through to the end of the file.
• OverlappingFileLockException: When a lock is already held for the File in question. Remember lock type will have an effect
on this exception happening or not.
• NonReadableChannelException: When the Channel is not opened for reading.
• NonWritableChannelException: When the Channel is not opened for writing.
• AsynchronousCloseException: All pending asynchronous IO operations terminate with this when the Channel has been closed.
• ClosedChannelException: When the Channel is closed and you try to initiate an IO operation.
The following code snippets demonstrate use of the AsynchronousFileChannel via the Future API for reading, writing and
locking. The samples are driven from unit tests all of which can be sourced from the download for this article.
AsynchronousFileChannel read sample
buffer.clear();
return read(channel, buffer, contents, filePosition + bytesRead);
} else {
return contents.toString();
}
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(UNABLE_TO_READ_CONTENTS, e);
}
}
• line 3-4: creates the AsynchronousFileChannel and calls the recursive read method with a newly constructed ByteBuffer.
• line 11: the method signature takes the position to continue reading from in each recursive routine.
• line 14: gets the result of the read, the number of bytes, blocks until the result is available.
• line 18: appends the contents of what was read from the ByteBuffer to the StringBuilder.
• line 20-21: clears the ByteBuffer prior to the next invocation and calls the method recursively again.
• line 16: gets the result of the write, the number of bytes written.
• line 18-21: loops while their are still bytes in the ByteBuffer and writes it out to file.
@Test
public void testSharedLock() throws IOException, InterruptedException, ExecutionException {
try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(Paths.get(this. ←-
filePath), StandardOpenOption.READ, StandardOpenOption.CREATE)) {
final FileLock lock = channel.lock(0, 0L, true).get();
@Test(expected = OverlappingFileLockException.class)
public void testOverlappingLock() {
final CountDownLatch innerThreadLatch = new CountDownLatch(1);
final CountDownLatch testThreadLatch = new CountDownLatch(1);
new Thread() {
public void run() {
try {
channel.lock().get();
innerThreadLatch.countDown();
testThreadLatch.await();
} catch (OverlappingFileLockException | ExecutionException ←-
| InterruptedException e) {
throw new RuntimeException("Unable to get lock on ←-
file for overlapping lock test", e);
}
}
}.start();
innerThreadLatch.await();
channel.lock().get();
} catch (InterruptedException | ExecutionException | IOException e) {
throw new RuntimeException(e);
} finally {
testThreadLatch.countDown();
}
}
• line 3: create the AsynchronousFileChannel ensuring we create the file if it does not already exist.
• line 4,6,13,15: obtains a FileLock in either shared or exclusive modes and validates that state.
Java NIO Programming Cookbook 55 / 73
• The final test, although not highlighted, is a test to prove an overlapping lock exception where two threads compete for the
same lock. Latches are used to ensure co-ordination between their competitive spirits. The takeaway from this last test is that
inside the same JVM process all Threads share the same locks, therefore trying to aquire an already held lock (exclusive) will
result in an OverlappingFileLockException. Using file locks to synchronize thread access to file regions will not work, however
in concert with normal thread synchronization and file locks one can achive co-ordinated access to files between threads and
processes.
7.7 AsynchronousServerSocketChannel
The AsynchronousChannelGroup is an abstraction that provides the AsynchronousServerSocketChannel with it’s thread pool
to handle it’s IO operations asynchronously. The AsynchronousServerSocketChannel also implements the NetworkChannel
interface which provides the ability to set channel SocketOption (more specifically StandardSocketOptions) values and to bind
to SocketAddress values.
• AsynchronousCloseException: All outstanding IO operations on the Channel terminate with said Exception when the Channel
has been closed.
• ClosedChannelException: Any new IO operations submitted after the Channel has been closed.
• NotYetBoundException: if accept() is called on a Channel instance that is not yet bound.
• ShutDownChannelGroupException: if the AsynchronousChannelGroup is already shutdown and a new IO operation is com-
menced.
• AcceptPendingException: if a thread calls accept() while another accept() call is still busy.
AsynchronousServerSocketChannel creation
...
private final AsynchronousServerSocketChannel server;
private final AsynchronousChannelGroup group;
...
public Server(final int port, final int poolSize, final String echo) {
try {
this.group = AsynchronousChannelGroup.withThreadPool(Executors. ←-
newFixedThreadPool(poolSize));
this.server = AsynchronousServerSocketChannel.open(this.group).bind(new ←-
InetSocketAddress(port));
...
• line 7-8: The AsynchronousServerSocketChannel is created with a AsynchronousChannelGroup supplied and a specified
poolSize .
Java NIO Programming Cookbook 56 / 73
• line 2-3: accept() is called and a requestKey and a CompletionHandler is supplied to handle the incoming connection.
The requestKey is a unique String generated for the purposes of establishing context in the multithreaded / asynchronous
Channel. The attachment in the completed(...) method call represents context and is actually the requestKey
being ushered into the CompletionHandler from the earlier accept() call.
• line 6: We are non-blocking and it is important to delegate off as soon as possible to handle the next incoming connection, a
unique key is generated (requestKey) which will later become the attachment (context) for the CompletionHandler.
• line 9: We handle the current connection by calling read(...) which will take the attachment for context and ultimately
create a new CompletionHandler for the purposes of reading the client request.
• line 12: If the IO operation fails, this method is called with the context and the reason for failure.
7.8 AsynchronousSocketChannel
The AsynchronousSocketChannel is an asynchronous Channel for connected sockets. Such a Channel has the ability to connect
to a remote address, read and write asynchronously, with the Future and CompletionHandler abstractions being provided as a
means for manipulating the outcomes of said IO operations. As per the AsynchronousServerSocketChannel, the Asynchronous-
SocketChannel also implements the NetworkChannel interface which provides the ability to set channel SocketOption (more
specifically StandardSocketOptions) values and to bind to SocketAddress values.
An AsynchronousSocketChannel can be opened via the two static open(...) methods: AsynchronousSocketChannel open
method #1
public static AsynchronousSocketChannel open(AsynchronousChannelGroup group) throws ←-
IOException
• AsynchronousCloseException: All pending asynchronous IO operations terminate with this when the Channel has been closed.
• ClosedChannelException: When the Channel is closed and you try to initiate an IO operation.
Java NIO Programming Cookbook 57 / 73
@Override
public void completed(final Void result, final String attachment) {
System.out.println(String.format("Client: Connect Completed in thread %s", ←-
Thread.currentThread().getName()));
updateMessageCache(attachment, StringUtils.EMPTY, Client.this.messageCache) ←-
;
write(channel, attachment);
}
@Override
public void failed(final Throwable exc, final String attachment) {
System.out.println(String.format("Client: Connect Failed in thread %s", ←-
Thread.currentThread().getName()));
exc.printStackTrace();
Client.this.latch.countDown();
closeChannel(channel);
}
});
}
...
private void write(final AsynchronousSocketChannel channel, final String requestId) {
assert !Objects.isNull(channel);
@Override
public void completed(final Integer result, final String attachment) {
System.out.println(String.format("Client: Write Completed in thread %s", ←-
Thread.currentThread().getName()));
Java NIO Programming Cookbook 58 / 73
read(channel, attachment);
}
• line 5: the AsynchronousSocketChannel is created supplying an AsynchronousChannelGroup upon creation for threading
purposes.
• line 6: a connection is attempted for the Channel supplying a unique String value as context for the connection.
• line 12-13: connect(...) is called and in particular the Channel’s ’ connect(...) is invoked passing a remoteAd
dress requestId and a CompletionHandler to handle the outcome of the IO operation. The requestId is the context
variable and manifests itself as the attachment in the CompletionHandler.
• line 20: write(...) is called passing the Channel upon which the connection was established and the context (attachment).
So effectively upon connection competion we commence an IO operation and as this is a client in a client server program the
first call of action is to write a request to the Server.
• line 29: we close the Channel upon failure to connect.
• line 42: write(...) is called on the Channel supplying a ByteBuffer as source, a context variable (requestId) and a
CompletionHandler.
7.9 Summary
In this tutorial we have covered the main abstractions in the asynchronous channels API, specifically focusing on the types of
AsnchronousChannel implementations, what they are and how to use them.
We have seen under what circumstances behavior could become exceptional (Exceptions) and how to manipulate the outcome of
IO operations on said Channels via "pending" and complete results.
Chapter 8
This article is a tutorial on implementing a simple Java NIO "echo server". This example will take the form of a rather simple
client server application whereby a client or many clients will connect to a running server and post message(s) to the server which
will in turn be "echoed" back to the respective clients.
8.1 Introduction
This article builds on two earlier articles on the subject of Java NIO, namely "Java Nio Tutorial for Beginners" and "Java Nio
Asynchronous Channels Tutorial" where we implement a simple "echo server" using some of the abstractions and techniques
discussed in the earlier articles.
The example code in this article was built and run using:
• Spring source tool suite 4.6.3 (Any Java IDE would work)
• Ubuntu 16.04 (Windows, Mac or Linux will do fine)
8.3 Overview
A server process is started with a port property specified at runtime. This server process listens for incoming connections from
potential client processes. Once an inbound connection from a client is detected the server process is notified of this and the
connection is accepted. The client is then able to send a message to the server. Upon receipt of this message the server is once
again notified and the server begins to read the incoming request, which when complete is subsequently sent back on the same
connection to the client, hence the "echo".
What follows are the code snippets of the all the abstractions used in this EchoServer implementation.
Java NIO Programming Cookbook 60 / 73
8.4.1 ChannelWriter
ChannelWriter
while (buffer.hasRemaining()) {
channel.write(buffer);
}
}
}
• line 8: we ensure their is still bytes remaining between the current position and the limit
• line 9: we attempt to write the remaining bytes in the ByteBuffer to the Channel
8.4.2 Client
Client
new Client(Integer.valueOf(args[0])).start(args[1]);
}
doWrite(buffer, client);
buffer.flip();
• line 20: using try(...) (with resources) we open a SocketChannel to the configured InetSocketAddress
• line 22: we create a ByteBuffer wrapping the contents of the specified message
• line 24: we call write(...) passing the ByteBuffer and the SocketChannel
• line 26: flipping the ByteBuffer to initialize the position and limit for reading
• line 29: call read(...) passing the StringBuilder (for placing the read contents into), the ByteBuffer and the SocketChannel
• line 37-44: we ensure we read everything from the Server
8.4.3 Server
Server
new Server(Integer.valueOf(args[0])).start();
}
while (!Thread.currentThread().isInterrupted()) {
if (selector.isOpen()) {
final int numKeys = selector.select();
if (numKeys > 0) {
handleKeys(channel, selector.selectedKeys());
}
} else {
Thread.currentThread().interrupt();
}
}
} catch (IOException e) {
throw new RuntimeException("Unable to start server.", e);
} finally {
this.session.clear();
}
}
channel.socket().setReuseAddress(true);
channel.configureBlocking(false);
channel.socket().bind(new InetSocketAddress(this.port));
channel.register(selector, SelectionKey.OP_ACCEPT);
}
iterator.remove();
}
}
}
cleanUp(key);
}
}
this.session.remove((SocketChannel) key.channel());
key.channel().close();
key.cancel();
}
}
• line 22: using try(...) (with resources) we open ServerSocketChannel and a Selector. The Selector will allow the Server
to multiplex over n number of SelectableChannel instances (ie: connections)
• line 23: we initialize the ServerSocketChannel and register it with the Selector. We also express interest in the Selection
Key.OP_ACCEPT IO operation meaning that the ServerSocketChannel will only be interested accepting connections
• line 27: call select() on the Selector, this is a blocking call and will only return when their are SelectionKey instances
(expressing IO events)
• line 29: handle the Set of SelectionKey instances from the select() call for the given ServerSocketChannel
• line 45: allows binding to the port even if a previous connection on that same port is still in a TIME_WAIT state
• line 46: ensure our Channel is in non-blocking mode for use by our Selector
• line 47: bind at the address
• line 48: register the Channel with the Selector
• line 59: whilst processing the keys ensure the SelectionKey is valid
• line 61: accept a new connection
• line 63: read from the connection
• line 71-76: ensure that after every IO event is handled we check if we must echo back to the client and if necessary cleanup
(close) recources etc. Ensure we remove the SelectionKey from the Set of SelectionKey instances otherwise we will continue
to process stale events
• line 84-89: for every incoming SocketChannel connection ensure we set blocking to false and express interest in Selectio
nKey.OP_READ IO events and create a new session
• line 99-100: if something was read - add it to the session buffer
• line 101-106: if the end of the stream has been reach, echo, if required to and clean up resources
The attached sample code is a maven project and can be compiled by executing the following: mvn clean install in the
project folder, assuming all packages / programs are installed. Then navigate to the target/classes folder within the project
folder and execute the following:
Start Server
Start Client
substituting the 9999 with any port number of your choosing and the Hello world! with any message of your choosing. If
successful you should see the following output:
Message : Hello world!
Echo : Hello world!
8.6 Summary
This example is demonstrated using the Selector class to multiplex over n number of SelectableChannels and echo back any mes-
sages received from said Channels. The Selector allowed our Server to handle the incoming IO events from said SelectableChan-
nels provided they were SelectionKey.OP_ACCEPT or SelectionKey.OP_READ ready. It managed a Session per
connected Channel and disposed of said Channel once the echo was complete.
Java NIO Programming Cookbook 65 / 73
Chapter 9
This article is a tutorial on demonstrating the usage of the Java Nio ByteBuffer. All examples are done in the form of unit tests
to easily prove the expectations of the API.
9.1 Introduction
The ByteBuffer class is an abstract class which also happens to extend Buffer and implement Comparable. A Buffer is simply a
linear finite sized container for data of a certain primitive type. It exhibits the following properties:
ByteBuffer has these properties but also displays a host of semantic properties of it’s own. According to the ByteBuffer API the
abstraction defines six categories of operations. They are:
• get(...) and put(...) operations that operate relatively (in terms of the current position) and absolutely (by supplying
an index)
• bulk get(...) operation done relatively (in terms of the current position) which will get a number of bytes from the
ByteBuffer and place it into the argument array supplied to the get(...) operation
• bulk put(...) operation done absolutely by supplying an index and the content to be inserted
• absolute and relative get(...) and put(...) operations that get and put data of a specific primitive type, making it
convenient to work with a specific primitive type when interacting with the ByteBuffer
• creating a "view buffer’ or view into the underlying ByteBuffer by proxying the underlying data with a Buffer of a specific
primitive type
• compacting, duplicating and slicing a ByteBuffer
The example code in this article was built and run using:
9.3 Overview
• allocate(int) this will allocate a HeapByteBuffer with the capacity specified by the int argument
• allocateDirect(int) this will allocate a DirectByteBuffer with the capacity specified by the int argument
The ByteBuffer class affords us the luxury of a fluent interface through much of it’s API, meaning most operations will return a
ByteBuffer result. This way we can obtain a ByteBuffer by also wrapping a byte [] , slicing a piece of another ByteBuffer,
duplicating an existing ByteBuffer and performing get(...) and put(...) operations against an existing ByteBuffer. I
encourage you to review the ByteBuffer API to understand the semantics of it’s API.
So why the distinction between direct and non-direct? It comes down to allowing the Operating System to access memory
addresses contiguously for IO operations (hence being able to shove and extract data directly from the memory address) as
opposed to leveraging the indirection imposed by the abstractions in the JVM for potentially non-contiguous memory spaces.
Because the JVM cannot guarantee contiguous memory locations for HeapByteBuffer allocations the Operating System
cannot natively shove and extract data into these types of ByteBuffers. So generally the rule of thumb is should you be doing a
lot of IO, then the best approach is to allocate directly and re-use the ByteBuffer. Be warned DirectByteBuffer instances
are not subject to the GC.
To ensure determinism we have been explicit about the Charset in use, therefore any encoding of bytes or decoding of bytes will
use the explicit UTF-16BE Charset.
Relative Get and Put operations Test cases
public class RelativeGetPutTest extends AbstractTest {
@Test
public void get() {
final ByteBuffer buffer = ByteBuffer.wrap("Hello world!".getBytes( ←-
BIG_ENDIAN_CHARSET));
@Test
public void put() {
Java NIO Programming Cookbook 68 / 73
buffer.put("H".getBytes(BIG_ENDIAN_CHARSET));
buffer.put("e".getBytes(BIG_ENDIAN_CHARSET));
buffer.put("l".getBytes(BIG_ENDIAN_CHARSET));
buffer.put("l".getBytes(BIG_ENDIAN_CHARSET));
buffer.put("o".getBytes(BIG_ENDIAN_CHARSET));
buffer.put(" ".getBytes(BIG_ENDIAN_CHARSET));
buffer.put("e".getBytes(BIG_ENDIAN_CHARSET));
buffer.put("a".getBytes(BIG_ENDIAN_CHARSET));
buffer.put("r".getBytes(BIG_ENDIAN_CHARSET));
buffer.put("t".getBytes(BIG_ENDIAN_CHARSET));
buffer.put("h".getBytes(BIG_ENDIAN_CHARSET));
buffer.put("!".getBytes(BIG_ENDIAN_CHARSET));
buffer.flip();
assertEquals("Text data invalid", "Hello earth!", byteBufferToString(buffer));
}
@Test
public void bulkGet() {
final ByteBuffer buffer = ByteBuffer.wrap("Hello world!".getBytes( ←-
BIG_ENDIAN_CHARSET));
final byte[] output = new byte[10];
buffer.get(output);
@Test
public void bulkPut() {
final ByteBuffer buffer = ByteBuffer.wrap("Hello world!".getBytes( ←-
BIG_ENDIAN_CHARSET));
final byte[] output = new String("earth.").getBytes(BIG_ENDIAN_CHARSET);
buffer.position(12);
buffer.put(output);
@Test
public void getChar() {
final ByteBuffer buffer = ByteBuffer.wrap("Hello world!".getBytes( ←-
BIG_ENDIAN_CHARSET));
buffer.mark();
buffer.reset();
assertEquals("’H’ not the first 2 bytes read", "H", new String(new byte[] { a, b }, ←-
BIG_ENDIAN_CHARSET));
assertEquals("Value and byte array not equal", Character.toString(value), new ←-
String(new byte[] { a, b }, BIG_ENDIAN_CHARSET));
}
@Test
public void putChar() {
final ByteBuffer buffer = ByteBuffer.wrap("Hello world!".getBytes( ←-
BIG_ENDIAN_CHARSET));
buffer.position(22);
buffer.putChar(’.’);
buffer.flip();
assertEquals("Text data invalid", "Hello world.", byteBufferToString(buffer));
}
}
The above suite of test cases demonstrate relative get() and put() operations. These have a direct effect on certain ByteBuffer
attributes (position and data). In addition to being able to invoke these operations with byte arguments or receive byte argu-
ments we also demonstrate usage of the putChar() and getChar(...) methods which conveniently act on the matching
primitive type in question. Please consult the API for more of these convenience methods
Absolute Get and Put operations Test cases
public class AbsoluteGetPutTest extends AbstractTest {
@Test
public void get() {
final ByteBuffer buffer = ByteBuffer.wrap("Hello world!".getBytes( ←-
BIG_ENDIAN_CHARSET));
@Test
public void put() {
final ByteBuffer buffer = ByteBuffer.wrap("Hello world!".getBytes( ←-
BIG_ENDIAN_CHARSET));
@Test
public void getChar() {
final ByteBuffer buffer = ByteBuffer.wrap("Hello world!".getBytes( ←-
BIG_ENDIAN_CHARSET));
char value = buffer.getChar(22);
@Test
public void putChar() {
final ByteBuffer buffer = ByteBuffer.wrap("Hello world!".getBytes( ←-
BIG_ENDIAN_CHARSET));
buffer.putChar(22, ’.’);
The above suite of test cases demonstrate usage of the absolute variants of the get(...) and put(...) operations. Inter-
estingly enough, only the underlying data is effected ( put(...) ) as the position cursor is not mutated owing to the method
signatures providing client code the ability to provide an index for the relevant operation. Again convenience methods which
deal with the various primitive types are also provided and we demonstrate use of the ...Char(...) variants thereof.
ViewBuffer Test cases
public class ViewBufferTest extends AbstractTest {
@Test
public void asCharacterBuffer() {
final ByteBuffer buffer = ByteBuffer.wrap("Hello world!".getBytes( ←-
BIG_ENDIAN_CHARSET));
final CharBuffer charBuffer = buffer.asCharBuffer();
@Test
public void asCharacterBufferSharedData() {
final ByteBuffer buffer = ByteBuffer.wrap("Hello world!".getBytes( ←-
BIG_ENDIAN_CHARSET));
final CharBuffer charBuffer = buffer.asCharBuffer();
);
}
}
In addition to the various convenience get(...) and put(...) methods that deal with the various primitive types ByteBuffer
provides us with an assortment of methods that provide primitive ByteBuffer views of the underlying data eg: asCharBuffe
r() demonstrates exposing a Character Buffer view of the underlying data.
Miscellaneous ByteBuffer Test cases
public class MiscBufferTest extends AbstractTest {
@Test
public void compact() {
final ByteBuffer buffer = ByteBuffer.allocate(24);
buffer.putChar(’H’);
buffer.putChar(’e’);
buffer.putChar(’l’);
buffer.putChar(’l’);
buffer.putChar(’o’);
buffer.flip();
buffer.position(4);
buffer.compact();
buffer.putChar(’n’);
buffer.putChar(’g’);
@Test
public void testDuplicate() {
final ByteBuffer buffer = ByteBuffer.wrap("Hello world!".getBytes( ←-
BIG_ENDIAN_CHARSET));
final ByteBuffer duplicate = buffer.duplicate();
buffer.putChar(22, ’.’);
@Test
public void slice() {
final ByteBuffer buffer = ByteBuffer.wrap("Hello world!".getBytes( ←-
BIG_ENDIAN_CHARSET));
buffer.position(12);
buffer.putChar(22, ’.’);
assertEquals("Text data invalid", "world.", byteBufferToString(sliced));
}
@Test
public void rewind() {
final ByteBuffer buffer = ByteBuffer.wrap("Hello world!".getBytes( ←-
BIG_ENDIAN_CHARSET));
buffer.rewind();
@Test
public void compare() {
final ByteBuffer a = ByteBuffer.wrap("Hello world!".getBytes(BIG_ENDIAN_CHARSET));
final ByteBuffer b = ByteBuffer.wrap("Hello world!".getBytes(BIG_ENDIAN_CHARSET));
@Test
public void compareDiffPositions() {
final ByteBuffer a = ByteBuffer.wrap("Hello world!".getBytes(BIG_ENDIAN_CHARSET));
final ByteBuffer b = ByteBuffer.wrap("Hello world!".getBytes(BIG_ENDIAN_CHARSET));
a.position(2);
9.5 Summary
In this tutorial we learned a bit about ByteBuffers, we understood how to create one, the various types, why we have the different
types and when to use them as well as the core semantic operations defined by the abstraction.
ByteBuffers are not thread safe and hence many operations on it, need to be guarded against to ensure that multiple threads do
not corrupt the data or views thereon. Be wary of relative get(...) and put(...) operations as these do sneaky things like
advancing the ByteBuffers position.
Wrapping, slicing and duplicating all point to the byte [] they wrapped or the ByteBuffer they sliced / duplicated. Changes
to the source input or the resulting ByteBuffers will effect each other. Luckily with slice(...) and duplicate(...) the
position, mark and limit cursors are independent.
When toggling between reading data into a ByteBuffer and writing the contents from that same ByteBuffer it is important to
flip() the ByteBuffer to ensure the limit is set to the current position , the current position is reset back to 0 and the
mark , if defined, is discarded. This will ensure the ensuing write will be able to write what was just read. Partial writes in this
context can be guarded against by calling compact() right before the next iteration of read and is very elegantly demonstrated
in the API under compact.
Java NIO Programming Cookbook 73 / 73
When comparing ByteBuffers the positions matter, ie: you can have segments of a ByteBuffer that are identical and these
compare favorably should the two ByteBuffers, in question, have the same position and limit ( bytesRemaining() ) during
comparison.
For frequent high volume IO operations a DirectByteBuffer should yield better results and thus should be preferred.
Converting a byte [] into a ByteBuffer can be accomplished by wrapping the byte [] via the wrap(...) method.
Converting back to a byte [] is not always that straight forward. Using the convenient array() method on ByteBuffer only
works if the ByteBuffer is backed by a byte [] . This can be confirmed via the hasArray() method. A bulk get(...)
into an applicably sized byte [] is your safest bet, but be on the guard for sneaky side effects, ie: bumping the position
cursor.