When I started learning standard I / O in Java, at first I was a bit shocked by the abundance of interfaces and classes of the
java.io. * package, supplemented by a whole list of specific exceptions.
Having spent a fair amount of hours studying and implementing a bunch of various tutorials from the Internet, I began to feel confident and sighed with relief. But at one point, I realized that everything is just beginning for me, since there is also a
java.nio. * Package, also known as Java NIO or Java New IO. At first it seemed that it was the same, well, like a side view. However, as it turned out, there are significant differences, both in principle of work, and in the tasks solved with their help.
The article by Jakob Jenkov -
“Java NIO vs. Io . Below it is given in an adapted form.
')
I hasten to note that the article is not a guide to using Java IO and Java NIO. Its goal is to give people starting to learn Java the ability to understand the conceptual differences between these two I / O tools.
The main differences between Java IO and Java NIO
Io | Nio |
---|
Flow oriented | Buffer oriented |
Blocking (synchronous) input / output | Non-blocking (asynchronous) I / O |
| Selectors |
Stream-oriented and buffer-oriented input / output
The main difference between the two approaches to organizing I / O is that Java IO is thread-oriented, and Java NIO is buffer-oriented. We will analyze in more detail.
A stream-oriented input / output implies reading / writing from a stream / stream of one or several bytes per unit of time alternately. This information is not cached anywhere. Thus, it is not possible to arbitrarily move forward or backward in a data stream. If you want to perform such manipulations, you must first cache the data in the buffer.
Flow Oriented Input:

Flow oriented output:

The approach on which Java NIO is based is slightly different. Data is read into a buffer for further processing. You can move back and forth along the buffer. This gives a bit more flexibility in data processing. At the same time, you need to check whether the buffer contains the amount of data necessary for correct processing. It is also necessary to ensure that when reading the data in the buffer you do not destroy the data that has not yet been processed, which are in the buffer.
Blocking and non-blocking I / O
I / O streams in Java IO are blocking. This means that when the read () or write () method of any class from the java.io. * package is called in the execution thread (tread), blocking occurs until data is read or written. The thread of execution at the moment can do nothing else.
Java NIO non-blocking mode allows you to request read data from a channel (channel) and receive only what is available at the moment, or nothing at all if there is no data available yet. Instead of remaining locked until the data is readable, the thread of execution can do something else.
ChannelsChannels are logical (not physical) portals through which data is input / output, and buffers are sources or receivers of this transmitted data. When organizing output, the data you want to send is buffered, and it is transferred to the channel. As you type, data from the channel is placed in the buffer you provided.
Channels resemble pipelines through which data is efficiently transported between byte buffers and entities on the other side of the channels. Channels are gateways that allow access to the operating system I / O services with minimal overhead, and buffers are the internal end points of these gateways used to transmit and receive data.
The same is true for non-blocking output. A thread of execution may request that some data be written to the channel, but not wait until it is completely recorded.
Thus, the non-blocking Java NIO mode allows you to use a single thread of execution for solving several tasks instead of idle waiting time in the locked state. The most common practice is to use the saved runtime for servicing I / O operations in another or other channels.
Selectors
Java NIO selectors allow a single thread of execution to monitor multiple input channels. You can register multiple channels with a selector, and then use a single execution thread to serve the channels that have data available for processing, or to select channels that are ready for recording.

To better understand the concept and benefit from the use of selectors, let's abstract from programming and imagine the railway station. Option without a selector: there are three railway tracks (canals), a train can arrive at each of them at any time (data from the buffer), an employee of the train station waits for each run (execution flow) whose task is to service the arriving train. As a result, three employees are constantly at the station, even if there are no trains at all. Option with the selector: the situation is the same, but for each platform there is an indicator that signals the station employee (execution flow) about the arrival of the train. Thus, the presence of one employee is sufficient at the station.
Impact of Java NIO and Java IO on application design
The choice between Java NIO and Java IO can for the following aspects of the design of your application:
1. API for accessing I / O classes;
2. Data processing;
3. The number of execution threads used to process the data.
I / O class reference API
Naturally, using Java NIO is seriously different from using Java IO. Since, instead of reading data bytes by byte using, for example, an InputStream, the data must first be read into a buffer and taken for processing from there.
Data processing
Data processing is also different when using Java NIO.
As mentioned, when using Java IO, you read data bytes per byte with an InputStream or Reader. Imagine that you are reading lines of textual information:
Name: Anna
Age: 25
Email: anna@mailserver.com
Phone: 1234567890
This stream of text lines can be processed as follows:
InputStream input = ... ; BufferedReader reader = new BufferedReader(new InputStreamReader(input)); String nameLine = reader.readLine(); String ageLine = reader.readLine(); String emailLine = reader.readLine(); String phoneLine = reader.readLine();
Notice how the state of the processing process depends on how far the program has progressed. When the first readLine () method returns the result of the execution, you are sure that a whole line of text has been read. The method is blocking and the blocking action continues until the entire line is read. You also clearly understand that this string contains a name. Likewise, when the method is called the second time, you know that you will get an age as a result.
As you can see, progress in program execution is achieved only when new data is available for reading, and for each step you know what kind of data it is. When the execution flow reaches the progress in reading a certain part of the data, the input flow (in most cases) no longer moves the data back. This principle is well demonstrated by the following scheme:

Implementation using Java IO will look a little different:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer);
Pay attention to the second line of code, in which the reading of bytes from the channel in the ByteBuffer takes place. When the result of executing this method is returned, you cannot be sure that all the data you need is inside the buffer. All you know is that the buffer contains some bytes. This makes processing a bit more difficult.
Imagine that after the first call to the read (buffer) method, only half of the line was read into the buffer. For example, “Name: An”. Can you process such data? Probably not. You will have to wait until at least one full line of text has been read into the buffer.
So how do you know if enough data contains a buffer for correct processing? No way. The only way to find out is to look at the data contained within the buffer. As a result, you will have to check the data in the buffer several times until it becomes available for proper processing. This is ineffective and may adversely affect the design of the program. For example:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer); while(! bufferFull(bytesRead) ) { bytesRead = inChannel.read(buffer); }
The bufferFull () method should monitor how much data is read into the buffer and return true or false, depending on whether the buffer is full or not. In other words, if the buffer is ready for processing, then it is considered full.
Also, the bufferFull () method must leave the buffer in an unchanged state, because otherwise the next portion of the read data may be written in the wrong place.
If the buffer is full, data from it can be processed. If it is not filled, you will still be able to process the data already existing in it, if this makes sense in your particular case. In most cases - it is pointless.
The following diagram demonstrates the process of determining the availability of data in the buffer for correct processing:

Results
Java NIO allows you to manage multiple channels (network connections or files) using the minimum number of threads. However, the cost of this approach is more complicated than using blocking streams, data parsing.
If you need to manage thousands of open connections at the same time, each of which transmits only a small amount of data, choosing an Java NIO for your application can be an advantage. This type of design is shown schematically in the following figure:

If you have fewer connections over which large amounts of data are transferred, then the classic I / O system design is the best choice:

It is important to understand that Java NIO is not a replacement for Java IO. It should be considered as an improvement - a tool that allows you to significantly expand the possibilities for organizing I / O. Proper use of the power of both approaches will allow you to build good high-performance systems.
It is worth noting that with the release of Java 1.7, Java NIO.2 also appeared, but its inherent innovations relate primarily to working with file I / O, so they are beyond the scope of this article.
PS: also in this post we used the materials of a very worthy article by Nino Guarnacci -
“Java.nio vs Java.io”