A comprehensive analysis of how Java NIO works

  • 2020-04-01 01:31:35
  • OfStack

In pieces   Input/output: conceptual description
Introduction to the I/O
The I/O? Or input/output? The interface between a computer and the outside world or between a program and the rest of the computer. It is so critical to any computer system that the body of all I/O is actually built into the operating system. Separate programs generally let the system do most of the work for them.
In Java programming, I/O was done in a streaming fashion until recently. All I/O is treated as a single byte move, one byte at a time, through an object called Stream. Stream I/O is used to contact the outside world. It is also used internally to convert objects to bytes and then back to objects.
NIO has the same function and purpose as the original I/O, but in a different way. Right? Block I/O. As you will learn in this tutorial, block I/O can be much more efficient than stream I/O.
Why use NIO?
NIO was created to allow Java programmers to implement high-speed I/O without having to write custom native code. NIO transfers the most time-consuming I/O operations (that is, filling and extracting buffers) back to the operating system, thus greatly increasing speed.
Flow versus block comparison
The most important difference between the original I/O library (in java.io.*) and NIO is the way data is packaged and transferred. As mentioned earlier, the original I/O processed data as a stream, while NIO processed data as a block.
Stream-oriented I/O systems process data one byte at a time. An input stream produces one byte of data, and an output stream consumes one byte of data. Creating filters for streaming data is easy. It is also relatively simple to link several filters so that each filter is responsible for only one part of a single complex processing mechanism. On the downside, stream-oriented I/O is usually quite slow.
A block-oriented I/O system processes data as blocks. Each operation produces or consumes a block of data in one step. Processing data in blocks is much faster than processing data in bytes. But block-oriented I/O lacks some of the elegance and simplicity of flow-oriented I/O.
Integration of I/O
The original I/O package and NIO were well integrated in JDK 1.4. Java.io.* has been reimplemented based on NIO, so it can now take advantage of some of NIO's features. For example, some classes in the java.io.* package contain methods to read and write data as blocks, which makes processing faster even in more stream-oriented systems.
You can also use the NIO library to implement standard I/O functionality. For example, you can easily use block I/O to move data one byte at a time. But as you can see, NIO also provides many benefits that were not available in the original I/O package.
In pieces channels and buffers
Almost   above
Channels and buffers are the core objects in NIO and are used in almost every I/O operation.
A channel is a simulation of the flow in the original I/O package. All data to any destination (or from anywhere) must pass through a Channel object. A Buffer is essentially a container object. All objects sent to a channel must be placed in a buffer first; Likewise, any data read from the channel is read into the buffer.
In this section, you'll learn how channels and buffers work in NIO.
What is a buffer?
A Buffer is an object that contains some data to be written or just read. Adding the Buffer object to NIO represents an important difference between the new library and the original I/O. In stream-oriented I/O, you write data directly or read it directly into the Stream object.
In the NIO library, all data is handled with buffers. When the data is read, it is read directly into the buffer. When data is written, it is written to the buffer. Any time you access data in NIO, you put it into a buffer.
A buffer is essentially an array. Usually it is a byte array, but other kinds of arrays can be used. But a buffer is more than just an array. Buffers provide structured access to data and can also track the system's read/write processes.
Buffer type
The most common type of buffer is ByteBuffer. A ByteBuffer can perform get/set operations (that is, get and set bytes) on its underlying byte array.
ByteBuffer is not the only buffer type in NIO. In fact, there is a buffer type for each of the basic Java types:
The & # 8226; ByteBuffer
The & # 8226; CharBuffer
The & # 8226; ShortBuffer
The & # 8226; IntBuffer
The & # 8226; LongBuffer
The & # 8226; FloatBuffer
The & # 8226; DoubleBuffer
Each Buffer class is an instance of the Buffer interface. With the exception of ByteBuffer, each of the Buffer classes has exactly the same operation, except that they handle different data types. Because most standard I/O operations use ByteBuffer, it has all the Shared buffer operations and some unique operations.
Now you can take a moment to run usefloatbuffer.java, which contains an example of using typed buffers.
What is a channel?
A Channel is an object through which data can be read and written. If you compare NIO to the original I/O, the channel is like a stream.
As mentioned earlier, all data is processed through the Buffer object. You never write bytes directly into a channel; instead, you write data to a buffer that contains one or more bytes. Again, instead of reading the byte directly from the channel, you read the data from the channel into the buffer and get the byte from the buffer.
Channel types
Channels differ from flows in that they are bidirectional. A stream just moves in one direction (a stream must be an InputStream or a subclass of OutputStream), and a channel can be used for reading, writing, or both.
Because they are bidirectional, channels can reflect the underlying operating system better than streams. In the UNIX model in particular, the underlying operating system channels are bidirectional.
In pieces from theory to practice: reading and writing in NIO
Almost   above
Reading and writing are the basic processes of I/O. Reading from a channel is simple: just create a buffer and have the channel read the data into that buffer. Writing is also fairly simple: create a buffer, fill it with data, and let the channel write with that data.
In this section, we'll learn a bit about reading and writing data in a Java program. We'll review the main components of NIO (buffers, channels, and some related methods) to see how they interact to read and write. In the next few sections, we'll examine each of these components and their interactions in more detail.
Read from a file
In our first exercise, we'll read some data from a file. If we use the original I/O, we just create a FileInputStream and read from it. In NIO, the situation is slightly different: we first get a FileInputStream object from the FileInputStream, and then use this channel to read the data.
In a NIO system, whenever a read operation is performed, you read from the channel, but you do not read directly from the channel. Because all the data ends up in the buffer, you read from the channel to the buffer.
So reading a file involves three steps :(1) getting the Channel from the FileInputStream, (2) creating a Buffer, and (3) reading the data from the Channel to the Buffer.
Now, let's look at the process.
Three easy steps
The first step is to get the channel. We get channels from the FileInputStream:

FileInputStream fin = new FileInputStream( "readandshow.txt" );  
FileChannel fc = fin.getChannel(); 

The next step is to create a buffer:

ByteBuffer buffer = ByteBuffer.allocate( 1024 ); 

Finally, you need to read the data from the channel into the buffer, as shown below:

fc.read( buffer ); 

You'll notice that we don't need to tell the channel how much data to read into the buffer. Each buffer has a complex internal statistics mechanism that tracks how much data has been read and how much room there is for more data
Written to the file
Writing to a file in NIO is similar to reading from a file. First get a channel from the FileOutputStream:

FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );  
FileChannel fc = fout.getChannel(); 

The next step is to create a buffer and put Some data in it - in this case, the data is pulled from an array called message, which contains the ASCII bytes of the string "Some bytes" (the buffer.flip() and buffer.put() calls will be explained later in the tutorial).

ByteBuffer buffer = ByteBuffer.allocate( 1024 );  
 for (int i=0; i
     buffer.put( message[i] );  
}  
buffer.flip(); 

The last step is to write to the buffer

fc.write( buffer ); 

Note that there is also no need to tell the channel to write multiple data. The buffer's internal statistics mechanism keeps track of how much data it contains and how much more to write.
Reading and writing combination
Now we'll see what happens when we combine reading and writing. This exercise is based on a simple program called copyfile.java, which copies everything from one file to another. Copyfile.java performs three basic operations: first create a Buffer, then read the data into the Buffer from the source file, and then write the Buffer to the target file. The program repeats the read, write, read, and write until the end of the source file.
The CopyFile program lets you see how we check the status of the operation and how we reset the buffer using the clear() and flip() methods and prepare the buffer to write the newly read data to another channel.
Run the CopyFile example
Because the buffer tracks its own data, the inner loop of the CopyFile program is very simple, as shown below:

fcin.read( buffer );  
fcout.write( buffer ); 

The first line reads the data into the buffer from the input channel fcin, and the second line writes the data to the output channel fcout.
Check the status
The next step is to check when the copy is complete. When there is no more data, the copy is complete and can be judged by returning -1 in the read() method, as shown below:

int r = fcin.read( buffer );  
 if (r==-1) {  
     break;  
} 

Reset buffer
Finally, we call the clear() method before we read the buffer from the input channel. Again, before writing the buffer to the output channel, we call the flip() method, as shown below

buffer.clear();int r = fcin.read( buffer );  
 if (r==-1) {  
     break;  
}  
 buffer.flip();  
fcout.write( buffer ); 

The clear() method resets the buffer so that it can accept data read in. The flip() method lets the buffer write newly read data to another channel.

Related articles: