145. Circular byte buffer

The Java NIO.2 API comes with an implementation of a byte buffer called java.nio.ByteBuffer. Basically, this is an array of bytes (byte[]) that's wrapped with a suite of methods dedicated to manipulating this array (for example, get(), put(), and so on). A circular buffer (cyclic buffer, ring buffer, or circular queue) is a fixed-size buffer that's connected end-to-end. The following diagram shows us what a circular queue looks like:

A circular buffer relies on a pre-allocated array (pre-allocated capacity), but some implementations may require a resizing capability as well. The elements are written/added to the back (tail) and removed/read from the front (head); this can be seen in the following diagram:

For the main operations, that is, read (get) and write (put), a circular buffer maintains a pointer (a read pointer and a write pointer). Both pointers are wrapped around the buffer capacity. We can find out how many elements are available to be read and how many free slots can be written whenever we like. This operation takes place in O(1).

A circular byte buffer is a circular buffer of bytes; it can be of chars or some other type. This is exactly what we want to implement here. We can start by writing a stub of our implementation, as follows:

public class CircularByteBuffer {

private int capacity;
private byte[] buffer;
private int readPointer;
private int writePointer;
private int available;

CircularByteBuffer(int capacity) {
this.capacity = capacity;
buffer = new byte[capacity];
}

public synchronized int available() {
return available;
}

public synchronized int capacity() {
return capacity;
}

public synchronized int slots() {
return capacity - available;
}

public synchronized void clear() {
readPointer = 0;
writePointer = 0;
available = 0;
}
...
}

Now, let's focus on putting (writing) new bytes and reading (getting) existing bytes. For example, a circular byte buffer that has a capacity of 8 can be represented like so:

Let's take a look at what's happening at each step:

  1. The circular byte buffer is empty and both pointers point to slot 0 (the first slot).
  2. We put the 5 bytes corresponding to hello in the buffer. readPointer remains in the same position, while writePointer points to slot 5.
  3. We get the bytes corresponding with the h, so readPointer moves to slot 1.
  4. Finally, we attempt to put the bytes of world in the buffer. This word is made up of 5 bytes, but we have only four free slots until we reach the buffer capacity. This means we can only write the bytes that correspond with world.

Now, let's take a look at the scenario in the following diagram:

From left to right, the steps are as follows::

  1. The first two steps are the same as the ones from the previous scenario.
  2. We get the bytes for hell. This will move readPointer to position 4.
  3. Finally, we put the bytes of world in the buffer. This time, the word fits in the buffer and writePointer moves to slot 2.

Based on this flow, we can easily implement a method that puts one byte in the buffer and another that gets one byte from the buffer, as follows:

public synchronized boolean put(int value) {
if (available == capacity) {
return false;
}

buffer[writePointer] = (byte) value;
writePointer = (writePointer + 1) % capacity;
available++;

return true;
}

public synchronized int get() {
if (available == 0) {
return -1;
}

byte value = buffer[readPointer];
readPointer = (readPointer + 1) % capacity;
available--;

return value;
}

If we check the Java NIO.2 ByteBuffer API, we'll notice that it exposes several flavors of the get() and put() methods. For example, we should be able to pass a byte[] to the get() method and this method should copy a range of elements from the buffer into this byte[]. The elements are read from the buffer, starting with the current readPointer, and are written in the given byte[], starting from the specified offset.

The following diagram exposes a case where writePointer is greater than readPointer:

On the left-hand side, we are reading 3 bytes. This moves readPointer from its initial slot, 1, to slot 4. On the right-hand side, we are reading 4 (or more than 4) bytes. Since there are only 4 bytes available, readPointer is moved from its initial slot to the same slot as writePointer (slot 5).

Now, let's analyze a case where writePointer is less than readPointer:

On the left-hand side, we are reading 3 bytes. This moves readPointer from its initial slot, 6, to slot 1. On the right-hand side, we are reading 4 (or more than 4) bytes. This moves readPointer from its initial slot, 6, to slot 2 (the same slot as writePointer).

Now that we have these two use cases in mind, we can write a get() method in order to copy a range of bytes from the buffer into the given byte[], as follows (this method attempts to read len bytes from the buffer and write them into the given byte[], starting from the given offset):

public synchronized int get(byte[] dest, int offset, int len) {

if (available == 0) {
return 0;
}

int maxPointer = capacity;

if (readPointer < writePointer) {
maxPointer = writePointer;
}

int countBytes = Math.min(maxPointer - readPointer, len);
System.arraycopy(buffer, readPointer, dest, offset, countBytes);
readPointer = readPointer + countBytes;

if (readPointer == capacity) {
int remainingBytes = Math.min(len - countBytes, writePointer);

if (remainingBytes > 0) {
System.arraycopy(buffer, 0, dest,
offset + countBytes, remainingBytes);
readPointer = remainingBytes;
countBytes = countBytes + remainingBytes;
} else {
readPointer = 0;
}
}

available = available - countBytes;

return countBytes;
}

Now, let's focus on putting the given byte[] into the buffer. The elements are read from the given byte[] starting from the specified offset and are written into the buffer starting from the current writePointer. The following diagram exposes a case where writePointer is greater than readPointer:

On the left-hand side, we have the initial state of the buffer. So, readPointer points to slot 2, and writePointer points to slot 5. After writing 4 bytes (on the right-hand side), we can see that readPointer was not affected and that writePointer points to slot 1.

The other use case assumes that readPointer is greater than writePointer:

On the left-hand side, we have the initial state of the buffer. So, readPointer points to slot 4, and writePointer points to slot 2. After writing 4 bytes (on the right-hand side), we can see that readPointer was not affected and that writePointer points to slot 4. Notice that only two bytes were successfully written. This has happened because we reached the maximum capacity of the buffer before writing all 4 bytes.

Now that we have these two use cases in mind, we can write a put() method in order to copy a range of bytes from the given byte[] into the buffer, as follows (the method attempts to read len bytes from the given byte[] starting from the given offset and attempts to write them into the buffer starting from the current writePointer):

public synchronized int put(byte[] source, int offset, int len) {

if (available == capacity) {
return 0;
}

int maxPointer = capacity;

if (writePointer < readPointer) {
maxPointer = readPointer;
}

int countBytes = Math.min(maxPointer - writePointer, len);
System.arraycopy(source, offset, buffer, writePointer, countBytes);
writePointer = writePointer + countBytes;

if (writePointer == capacity) {
int remainingBytes = Math.min(len - countBytes, readPointer);

if (remainingBytes > 0) {
System.arraycopy(source, offset + countBytes,
buffer, 0, remainingBytes);
writePointer = remainingBytes;
countBytes = countBytes + remainingBytes;
} else {
writePointer = 0;
}
}

available = available + countBytes;

return countBytes;
}

As we mentioned earlier, sometimes, we need to resize the buffer. For example, we may want to double its size by simply calling the resize() method. Basically, this means copying all the available bytes (elements) into a new buffer with double capacity:

public synchronized void resize() {

byte[] newBuffer = new byte[capacity * 2];

if (readPointer < writePointer) {
System.arraycopy(buffer, readPointer, newBuffer, 0, available);
} else {
int bytesToCopy = capacity - readPointer;
System.arraycopy(buffer, readPointer, newBuffer, 0, bytesToCopy);
System.arraycopy(buffer, 0, newBuffer, bytesToCopy, writePointer);
}

buffer = newBuffer;
capacity = buffer.length;
readPointer = 0;
writePointer = available;
}

Check the source code bundled with this book to see how it works in full.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset