Java NIO (New IO), introduced by Java 1.4, is an alternative IO API for Java. It offers a different way of working with IO than the standard IO API’s.

Three core components of Java NIO cover:

  • Channels
  • Buffers
  • Selectors

All IO in NIO involves with Channel. Data can be read from Channel to a Buffer and vice versa.

Buffers are used to interact with Channels. A Buffer is an allocated block of memory with fixed size, into which you can read or write data. In this post, I’ll illustrate the basic usage of Buffer and elucidate how it manages to do that from the view of source code.

Basic Usage

Using a Buffer to read and write data typically follows this little 4-step process:

  1. Write data into the Buffer
  2. Call buffer.flip()
  3. Read data out of the Buffer
  4. Call buffer.clear() or buffer.compact()

It’s hard to understand what flip() does from its name. Actually, flip() switch the buffer from writing mode to reading mode, allowing reading all data in it.

After read, you need to clear the buffer so that it can be written again. Two alternative methods, clear() and compact(), are able to sweep the buffer for you. While clear() clears the whole buffer, compact() only clear the data just already read. Any unread data is moved to the beginning of buffer, after which data will be written.

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {

  buf.flip();  //make buffer ready for read

  while(buf.hasRemaining()){
      System.out.print((char) buf.get()); // read 1 byte at a time
  }

  buf.clear(); //make buffer ready for writing
  bytesRead = inChannel.read(buf);
}
aFile.close();

Buffer Capacity, Position and Limit

Aside from its content, the essential properties of a buffer are

  • capacity: the number of elements it contains. The capacity of a buffer is fixed, which means it nevew changes.
  • limit: the index of the first element that should not be read or written. A limit is never greater than its capacity.
  • position: the index of the next element to be read or written. Position is always smaller that its limit.

A glimpse of code supports the above statements.

public abstract class Buffer {
    private int position = 0;
    private int limit;
    private int capacity;
}

Java NIO provides various types of buffers that can be used depending on the type of input. This post will take ByteBuffer for simplicity.

Allocating a Buffer

Before you do anything on a Buffer, you need to allocate one. allocate method is inevitable to call.

The following example shows the allocation of a ByteBuffer, with a capacity of 48 bytes.

ByteBuffer buf = ByteBuffer.allocate(48);

allocate in ByteBuffer calls the constructor of HeapByteBuffer, a subclass of `ByteBuffer.

// ByteBuffer.java

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

A byte array with capacity of cap is allocated and offet is set to 0, here is a illustration.

hb = new byte[cap];
offset = 0;

hb and offset are two varibles in ByteBuffer.

public abstract class ByteBuffer
    extends Buffer
    implements Comparable<ByteBuffer>
{
    final byte[] hb;                  // Non-null only for heap buffers
    final int offset;
}

Writing Data to a Buffer

You can write data from a Channel to a Buffer, like

int bytesRead = inChannel.read(buf); //read into buffer

Or you can write data into Buffer yourself,

buf.put(127);

The former will be illuminated later. Let’s check what put does.

// ByteBuffer.java

public abstract ByteBuffer put(byte b);

put method is abstract in ByteBuffer, which means its derived class is responsible to implement it.

As mentioned before, HeapByteBuffer derives from ByteBuffer.

// HeapByteBuffer.java

class HeapByteBuffer
    extends ByteBuffer 
{
    public ByteBuffer put(byte x) {

        hb[ix(nextPutIndex())] = x;
        return this;
    }

    protected int ix(int i) {
        return i + offset;
    }

    final int nextPutIndex() {                          // package-private
        if (position >= limit)
            throw new BufferOverflowException();
        return position++;
    }
}

nextPutIndex returns the current position against the limit, and ix adds any input with offset. The combination of nextPutIndex and ix find the index which the newcome byte can be put into.

flip()

flip method swithes the mode of a Buffer, from writing mode to reading.

// Buffer.java

public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

The code is simple, from which we can see it sets the limit to current position and then sets the position back to 0.

After flip(), position represents the reading point and limit indicates how many bytes have been written into this Buffer so that can be read.

Reading Data from a Buffer

get method gives a way to read data from Buffer.

byte aByte = buf.get();

Here is what HeapByteBuffer does.

// HeapByteBuffer.java

public byte get() {
    return hb[ix(nextGetIndex())];
}

final int nextGetIndex() {
    if (position >= limit)
        throw new BufferUnderflowException();
    return position++;
}

nextGetIndex checks the current position against the limit, and plus one after each read.

rewind()

The Buffer.rewind() sets the position back to 0 and keeps limit intact, so you can reread all the data in the buffer.

// Buffer.java

public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}

clear()

clear() does not purge the Buffer, as many of you may think. It only resets position to 0, and limit to capacity. Any unread data will lost if you clear() wrongly.

// Buffer.java

public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

If you want to keep those unread data, use compact(). compact() copies all unread data to the beginning of the Buffer. Then it sets position to right after the last unread element. The limit property is still set to capacity, just like clear() does. Now the Buffer is ready for writing, but you will not overwrite the unread data.

// HeapByteBuffer.java

public ByteBuffer compact() {
    System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
    position(remaining());
    limit(capacity());
    discardMark();
    return this;
}

mark

You may notice, no matter in get, put, clear or compact, a variable mark is engaged. As its name reveals, you can mark a given position of Buffer if you want.

By calling mark(), current position is assigned to mark, which you can acquire later on.

// Buffer.java

public final Buffer mark() {
    mark = position;
    return this;
}

See markValue for read and reset to resets this buffer’s position to the previously-marked position.

// Buffer.java

final int markValue() { 
    return mark;
}

public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}

equals() and compareTo()

Another method in Buffer that must be refferd is equals(). It is more complicated comparing the equality of two Buffers.

// ByteBuffer.java
public boolean equals(Object ob) {
    if (this == ob)
        return true;
    if (!(ob instanceof ByteBuffer))
        return false;
    ByteBuffer that = (ByteBuffer)ob;
    if (this.remaining() != that.remaining())
        return false;
    int p = this.position();
    for (int i = this.limit() - 1, j = that.limit() - 1; i >= p; i--, j--)
        if (!equals(this.get(i), that.get(j)))
            return false;
    return true;
}

Two buffers are equal if:

  1. They are of the same type (byte, char, int etc.)
  2. They have the same amount of remaining bytes, chars etc. in the buffer.
  3. All remaining bytes, chars etc. are equal.

It’s inevitable to involve compareTo() since equals() appears.

// ByteBuffer.java

public int compareTo(ByteBuffer that) {
    int n = this.position() + Math.min(this.remaining(), that.remaining());
    for (int i = this.position(), j = that.position(); i < n; i++, j++) {
        int cmp = compare(this.get(i), that.get(j));
        if (cmp != 0)
            return cmp;
    }
    return this.remaining() - that.remaining();
}

A buffer is considered “smaller” than another buffer if:

  1. The first element which is equal to the corresponding element in the other buffer, is smaller than that in the other buffer.
  2. All elements are equal, but the first buffer runs out of elements before the second buffer does (it has fewer elements).

Reference