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:
- Write data into the Buffer
- Call
buffer.flip()
- Read data out of the Buffer
- Call
buffer.clear()
orbuffer.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:
- They are of the same type (byte, char, int etc.)
- They have the same amount of remaining bytes, chars etc. in the buffer.
- 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:
- The first element which is equal to the corresponding element in the other buffer, is smaller than that in the other buffer.
- All elements are equal, but the first buffer runs out of elements before the second buffer does (it has fewer elements).
Reference