This topic is the beginning of your journey into the NIO (New I/O), an API introduced in Java 4 version that is located in the java.nio package. You will learn how data writing and reading are organized here, which classes are involved in it, and how they are related to each other. In the end, you will see an example of working with a file using these classes.
Fundamental concepts
While in the standard I/O you work with byte and char streams, this API operates on channels and a buffer. It provides the functionality of operating not only with files but also network sockets. In this topic, you'll read about three fundamental components of NIO. They are:
- Buffer
- Channel
- Selector
The main idea of this new API is the possibility of non-blocking operations and the reuse of threads for working with multiple data sources. Let's explore its components one by one to understand the role of each of them.
Buffer
In this API, a program doesn't contact a data source directly. It reads and writes the data via buffers, which in their turn communicate with the data source. They are intermediate containers between a program and a data source, acting as a data holder.
A buffer itself is a block of memory stored in a buffer object of a specific primitive type. Classes representing buffer types in Java 19 are:
ByteBuffer/MappedByteBufferCharBufferDoubleBufferFloatBufferIntBufferLongBufferShortBuffer
All these classes are located in the java.nio package and extend the Buffer class, except for MappedByteBuffer, which extends the ByteBuffer class.
Buffer class, these classes also implement several interfaces.Channel
Channels are responsible for providing buffers with a connection to the data source. While a regular API has a separate InputStream and OutputStream to read from the data source or write the data, channels can operate in both directions.
The diagram above shows what role the channel plays when communicating with the data source. If the program wants to write data to a data source, it writes it to the buffer, and the channel reads the data from the buffer to then write it to the source. When you need to read from a data source, the channel connects to it, reads the required information, writes it to the buffer, and the program reads that data.
Channel implementations are:
FileChannelfor connecting to files.DatagramChannelfor operating with data over the network using the UDP protocol.SocketChannelprovides the functionality to operate with data over the network using the TCP protocol. It implements a client socket.ServerSocketChannelprovides the functionality to operate with data over the network using the TCP protocol. It implements a server socket.
All classes and interfaces involved in channel operation are located in the java.nio.channels package.
Their hierarchy is more complex than that of buffers: the core hierarchy is shown in the diagram above. Of course, these classes also implement interfaces.
Selector
At the beginning of the topic, we mentioned that this API can perform non-blocking I/O operations. This feature is implemented using selectors and selection keys. They let you perform input and output operations through multiple channels using a single thread. It is achieved by monitoring channel states and notifying the thread about any changes.
Classes implementing selector behavior are located in the same java.nio.channels package, just like channels.
In traditional I/O, we need a thread for each data source. Considering that most of the time these threads are waiting for input, the idea of reusing a single thread makes sense if there are many data sources.
NIO usage example
Now let's look at a small basic example of working with files. We won't go into details, the goal is just to show you the difference between regular I/O and NIO when connecting to a file.
public static void main(String[] args) throws IOException {
try (RandomAccessFile file = new RandomAccessFile("file.txt", "r")) {
FileChannel channel = file.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(20);
int bytes;
while ((bytes = channel.read(buffer)) > 0) {
System.out.printf("%2d bytes read: ", bytes);
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println();
buffer.clear();
}
}
}
As you can see, the code here is more complex than it could be when using the old I/O API. In the future, you will have a better understanding of how this code works.
Conclusion
In this topic, you learned about the fundamental operation structure of the NIO API. You got an idea of its key components and what role each of them performs. This is only the first step towards mastering the I/O approach. There is much more to come for a full understanding of the topic — stay tuned!