Earlier, we got to know the IO API (Input/Output Application Programming Interface) and the java.io package, whose classes are mainly for working with streams in Java. The key here is the concept of a stream.

Today we'll begin to consider the NIO API (New Input/Output).

The main difference between the two approaches to I/O is that the IO API is stream-oriented while the NIO API is buffer-oriented. So the main concepts to understand are buffers and channels.

What's a buffer and what's a channel?

A channel is a logical portal through which data moves in and out, while a buffer is the source or destination of this transmitted data. During output, the data you want to send is put into a buffer, and the buffer passes the data to the channel. During input, the data from the channel gets put into the buffer.

In other words:

  • a buffer is simply a block of memory into which we can write information and from which we can read information,
  • a channel is a gateway that provides access to I/O devices such as files or sockets.

Channels are very similar to streams in the java.io package. All data that goes anywhere (or comes from anywhere) must pass through a channel object. In general, to use the NIO system, you get a channel to an I/O entity and a buffer for storing data. Then you work with the buffer, inputting or outputting data as needed.

You can move forward and backward in a buffer, i.e. "walk" the buffer, which is something you could not do in streams. This gives more flexibility when processing data. In the standard library, buffers are represented by the abstract Buffer class and several of its descendants:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • FloatBuffer
  • DoubleBuffer
  • LongBuffer

The main difference between the subclasses is the data type they store — bytes, ints, longs and other primitive data types.

Buffer properties

A buffer has four main properties. These are capacity, limit, position, and mark.

Capacity is the maximum amount of data/bytes that can be stored in the buffer. A buffer's capacity cannot be changed. Once a buffer is full, it must be cleared before writing more to it.

In write mode, a buffer's limit is the same as its capacity, indicating the maximum amount of data that can be written to the buffer. In read mode, a buffer's limit refers to the maximum amount of data that can be read from the buffer.

The position indicates the current position of the cursor in the buffer. Initially, it is set to 0 when the buffer is created. In other words, it is the index of the next element to be read or written.

The mark is used to save a cursor position. As we manipulate a buffer, the cursor position changes constantly, but we can always return it to the previously marked position.

Methods for working with a buffer

Now let's look at the main set of methods that let us work with our buffer (memory block) for reading and writing data to and from channels.

  1. allocate(int capacity) — this method is used to allocate a new buffer with the specified capacity. The allocate() method throws an IllegalArgumentException if the passed capacity is a negative integer.

  2. capacity() returns the current buffer's capacity.

  3. position() returns the current cursor position. Read and write operations move the cursor to the end of the buffer. The return value is always less than or equal to the limit.

  4. limit() returns the current buffer's limit.

  5. mark() is used to mark (save) the current cursor position.

  6. reset() returns the cursor to the previously marked (saved) position.

  7. clear() sets the position to zero and sets the limit to the capacity. This method does not clear the data in the buffer. It only reinitializes the position, limit, and mark.

  8. flip() switches the buffer from write mode to read mode. It also sets the limit to the current position and then puts the position back at zero.

  9. read() — The channel's read method is used to write data from the channel to the buffer, while the buffer's put() method is used to write data to the buffer.

  10. write() — The channel's write method is used to write data from the buffer to the channel, while buffer's get() method is used to read data from the buffer.

  11. rewind() rewinds the buffer. This method is used when you need to reread the buffer — it sets the position to zero and does not change the limit.

And now a few words about channels.

The most important channel implementations in Java NIO are the following classes:

  1. FileChannel — A channel for reading and writing data from/to a file.

  2. DatagramChannel — This class reads and writes data over the network via UDP (User Datagram Protocol).

  3. SocketChannel — A channel for reading and writing data over the network via TCP (Transmission Control Protocol).

  4. ServerSocketChannel — A channel for reading and writing data over TCP connections, just as a web server does. A SocketChannel is created for each incoming connection.

Practice

It's time to write a couple of lines of code. First, let's read the file and display its contents on the console, and then write some string to the file.

The code contains a lot of comments — I hope they will help you understand how everything works:

// Create a RandomAccessFile object, passing in the file path
// and a string that says the file will be opened for reading and writing
try (RandomAccessFile randomAccessFile = new RandomAccessFile("text.txt", "rw");
    // Get an instance of the FileChannel class
    FileChannel channel = randomAccessFile.getChannel();
) {
// Our file is small, so we'll read it in one go
// Create a buffer of the required size based on the size of our channel
   ByteBuffer byteBuffer = ByteBuffer.allocate((int) channel.size());
   // Read data will be put into a StringBuilder
   StringBuilder builder = new StringBuilder();
   // Write data from the channel to the buffer
   channel.read(byteBuffer);
   // Switch the buffer from write mode to read mode
   byteBuffer.flip();
   // In a loop, write data from the buffer to the StringBuilder
   while (byteBuffer.hasRemaining()) {
       builder.append((char) byteBuffer.get());
   }
   // Display the contents of the StringBuilder on the console
   System.out.println(builder);

   // Now let's continue our program and write data from a string to the file
   // Create a string with arbitrary text
   String someText = "Hello, Amigo!!!!!";
   // Create a new buffer for writing,
   // but let the channel remain the same, because we're going to the same file
   // In other words, we can use one channel for both reading and writing to a file
   // Create a buffer specifically for our string — convert the string into an array and get its length
   ByteBuffer byteBuffer2 = ByteBuffer.allocate(someText.getBytes().length);
   // Write our string to the buffer
   byteBuffer2.put(someText.getBytes());
   // Switch the buffer from write mode to read mode
   // so that the channel can read from the buffer and write our string to the file
   byteBuffer2.flip();
   // The channel reads the information from the buffer and writes it to our file
   channel.write(byteBuffer2);
} catch (FileNotFoundException e) {
   e.printStackTrace();
} catch (IOException e) {
   e.printStackTrace();
}

Try the NIO API — you'll love it!