Key differences between Java IO and Java NIO

When I began to study standard input / output in Java, at first I was a little shocked by the abundance of interfaces and classes of the * 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 breathed a sigh of relief. But at one point I realized that everything was just beginning for me, since there is also the java.nio. * Package , also known as Java NIO or Java New IO. At first it seemed like the same thing, like a side view. However, as it turned out, there are significant differences, both in the principle of work and in the tasks solved with their help.

An 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 purpose is to give people who are starting to learn Java the opportunity to understand the conceptual differences between the two specified tools for organizing I / O.

The main differences between Java IO and Java NIO

Stream orientedBuffer oriented
Blocking (synchronous) input / outputNon-Blocking (Asynchronous) I / O

Stream oriented and buffer oriented input / output

The main difference between the two approaches to organizing I / O is that Java IO is stream-oriented, and Java NIO is buffer-oriented. We will analyze in more detail.

Stream-oriented I / O involves reading / writing from / to a stream of one or more bytes per unit of time in turn. This information is not cached anywhere. Thus, it is not possible to arbitrarily move forward or backward in the data stream. If you want to perform such manipulations, you will have to cache the data in the buffer first.

Flow Oriented Input:

Flow Oriented Output:

The approach on which Java NIO is based is slightly different. Data is read into the buffer for further processing. You can move forward and backward through the buffer. This gives a little more flexibility in data processing. At the same time, you need to check whether the buffer contains the amount of data necessary for the correct processing. It is also necessary to ensure that when reading data into the buffer you do not destroy the data that has not yet been processed 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 * package is called in the thread (tread), a lock occurs until the data is read or written. The thread of execution at the moment cannot do anything else.

Non-blocking mode Java NIO allows you to request read data from the channel (channel) and receive only what is currently available, or nothing at all if there is no data available. Instead of staying locked until the data becomes readable, the thread of execution may do something else.

Channels are logical (non-physical) portals through which data is input / output, and buffers are the sources or receivers of these transmitted data. When organizing the output, the data that you want to send is placed in the buffer, and it is transferred to the channel. When entering, data from the channel is placed in the buffer you provided.

Channels resemble pipelines that efficiently transport data between byte buffers and entities on the other side of the channels. Channels are gateways that allow you to access the I / O services of the operating system with minimal overhead, and buffers are the internal endpoints of these gateways used to send and receive data.

The same is true for non-blocking output. The thread of execution may request some data to be written to the channel, but not wait for it to be completely written.

Thus, the non-blocking mode of Java NIO allows you to use one thread of execution to solve several tasks instead of empty burning time to wait in locked states. The most common practice is to use the saved run time of a workflow to service I / O operations in another or other channels.


Selectors in Java NIO allow a single thread of execution to monitor multiple input channels. You can register several channels with a selector, and then use one flow of execution to service channels that have data available for processing, or to select channels that are ready for recording.

To better understand the concept and the benefits of using selectors, let's ignore programming and imagine a train station. Option without a selector: there are three railway tracks (channels), a train (data from the buffer) can arrive at each of them at any time, a station employee (execution flow) is constantly waiting on each track, whose task is to service the train that arrived. As a result, three employees are constantly at the station, even if there are no trains at all. The option with the selector: the situation is the same, but for each platform there is an indicator signaling to the station employee (flow of execution) about the arrival of the train. Thus, the presence of one employee is sufficient at the station.

The Impact of Java NIO and Java IO on Application Design

The choice between Java NIO and Java IO can affect the following design aspects of your application:
1. I / O class calls API;
2. Data processing;
3. The number of execution threads used to process the data.

I / O Class Access API

Naturally, using Java NIO is very different from using Java IO. Since, instead of reading data byte by byte using, for example, InputStream, the data must first be read into the buffer and taken from there already for processing.

Data processing

Data processing when using Java NIO is also different.
As already mentioned, when using Java IO, you read data byte by byte with InputStream or Reader. Imagine that you are reading lines of text information:

Name: Anna
Age: 25
Phone: 1234567890

This stream of lines of text 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 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 line contains a name. Similarly, when the method is called a second time, you know that you get 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 thread of progress makes progress in reading a certain part of the data, the input stream (in most cases) no longer moves the data backward. 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 =;

Pay attention to the second line of code, in which bytes are read from the channel in ByteBuffer. 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 complicates the processing a bit.

Imagine that after the first call to the read (buffer) method, only half the line was read into the buffer. For example, “Name: An”. Will you be able to 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 there is enough data for the correct processing of the buffer? But 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 several times in the buffer until they become available for correct processing. This is inefficient and may adversely affect program design. For instance:

ByteBuffer buffer = ByteBuffer.allocate(48);
	int bytesRead =;
		while(! bufferFull(bytesRead) ) {
		    bytesRead =;

The bufferFull () method should monitor how much data has been 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 should leave the buffer unchanged, because otherwise the next portion of the read data may be written to 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 in it, if that makes sense in your particular case. In most cases, this is pointless.

The following diagram demonstrates the process of determining the readiness of data in a buffer for correct processing:


Java NIO allows you to manage multiple channels (network connections or files) using a minimum number of threads. However, the price 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, and each of them transfers only a small amount of data, choosing Java NIO for your application can be an advantage. This type of design is schematically depicted in the following figure:

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

It’s important to understand that Java NIO is by no means a replacement for Java IO. It should be considered as an improvement - a tool that allows you to significantly expand the ability to organize input / output. 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, first of all, to work with file I / O, so they are beyond the scope of this article.

PS: the materials of a very worthy article by Nino Guarnacci - “Java.nio vs” are also used in this post

Also popular now: