ByteBuffer How to use flip() and compact() - Feature image

Java ByteBuffer Example: How to Use flip() and compact()

Author image
by Sven WoltmannFebruary 26, 2020

In this article, I will show you, with an example, how Java’s ByteBuffer works and what the methods flip() and compact() do precisely.

The article answers the following questions:

  • What is a ByteBuffer, and what do you need it for?
  • How to create a ByteBuffer?
  • What do the values position, limit, and capacity mean?
  • How to write into the ByteBuffer, how to read from it?
  • What exactly do the methods flip() and compact() do?

Let’s go!

What Is a Bytebuffer, and What Do We Need It For?

You need a ByteBuffer to write data to or read data from a file, a socket, or another I/O component using a so-called “Channel”.

(This article is mainly about ByteBuffer itself. To learn how to write and read files with ByteBuffer and FileChannel, see the “FileChannel” article in the “Files” tutorial).

A ByteBuffer is a wrapper around a byte array and provides methods for convenient writing to and reading from the byte array. The ByteBuffer internally stores the read/write position and a so-called “limit”.

You will learn what this means in the following example – step-by-step.

You can find the code written for this article in this GitHub Repository.

How to Create a ByteBuffer

First, you must create a ByteBuffer with a given size (“capacity”). There are two methods for this:

  • ByteBuffer.allocate(int capacity)
  • ByteBuffer.allocateDirect(int capacity)

The capacity parameter specifies the size of the buffer in bytes.

The allocate() method creates the buffer in the Java heap memory, where the Garbage collector will remove it after use.

allocateDirect(), on the other hand, creates the buffer in native memory, i.e., outside the heap. Native memory has the advantage that read and write operations are executed faster. The reason is that the corresponding operating system operations can access this memory area directly, and data does not have to be exchanged between the Java heap and the operating system first. The disadvantage of this method is higher allocation and deallocation costs.

We create a ByteBuffer with a size of 1,000 bytes as follows:

var buffer = ByteBuffer.allocate(1000);
Code language: Java (java)

Then we have a look at the buffer’s metrics – position, limit, and capacity:

Since we will repeatedly print these metrics throughout the example, we create a printMetrics method for them:

private static void printMetrics(ByteBuffer buffer) { System.out.printf("position = %4d, limit = %4d, capacity = %4d%n", buffer.position(), buffer.limit(), buffer.capacity()); }
Code language: Java (java)

After creating the ByteBuffer, we see the following output:

position = 0, limit = 1000, capacity = 1000
Code language: plaintext (plaintext)

Here is a graphical representation so that you can better imagine the buffer. The yellow area is empty and can subsequently be filled.

ByteBuffer mit position = 0, limit = 1000,  capacity = 1000

ByteBuffer Position, Limit, and Capacity

The printed metrics mean:

  • position is the read/write position. It is always 0 for a new buffer.
  • limit has two meanings: When we write to the buffer, limit indicates the position up to which we can write. When we read from the buffer, limit indicates up to which position the buffer contains data. Initially, a ByteBuffer is always in write mode, and limit is equal to capacity – we can fill the empty buffer up to the end.
  • capacity indicates the size of the buffer. Its value of 1,000 corresponds to the 1,000 that we passed to the allocate() method. It will not change during the lifetime of the buffer.

The ByteBuffer Read-Write Cycle

A complete read-write cycle consists of the steps put(), flip(), get() and compact(). We will look at these in the following sections.

Writing to the ByteBuffer Using put()

For writing into the ByteBuffer, there are several put() methods to write single bytes, a byte array, or other primitive types (like char, double, float, int, long, short) into the buffer.

In our example, we write 100 times the value 1 into the buffer, and then we look at the buffer metrics again:

for (int i = 0; i < 100; i++) { buffer.put((byte) 1); } printMetrics(buffer);
Code language: Java (java)

After running the program, we see the following output:

position = 100, limit = 1000, capacity = 1000
Code language: plaintext (plaintext)

The position has moved 100 bytes to the right; the buffer now looks as follows:

ByteBuffer with position = 100, limit = 1000,  capacity = 1000

Next, we write 200 times a two in the buffer. We use a different method this time: We first fill a byte array and copy it into the buffer. Finally, we print the metrics again:

byte[] twos = new byte[200]; Arrays.fill(twos, (byte) 2); buffer.put(twos); printMetrics(buffer);
Code language: Java (java)

Now we see:

position = 300, limit = 1000, capacity = 1000
Code language: plaintext (plaintext)

The position has shifted another 200 bytes to the right; the buffer looks like this:

ByteBuffer with position = 300, limit = 1000,  capacity = 1000

Switching to Read Mode with Buffer.flip()

For reading from the buffer, there are corresponding get() methods. These are invoked, for example, when writing to a channel using Channel.write(buffer).

Since position indicates not only the write position but also the read position, we must set position back to 0.

At the same time, we set limit to 300 to indicate that one can read a maximum of 300 bytes from the buffer.

In the program code, we do this as follows:

buffer.limit(buffer.position()); buffer.position(0);
Code language: Java (java)

Since these two lines are needed every time you switch from write to read mode, there is a ByteBuffer method that does exactly the same for us:

buffer.flip();
Code language: Java (java)

Invoking printMetrics() now shows the following values:

position = 0, limit = 300, capacity = 1000
Code language: plaintext (plaintext)

The position pointer has returned to the beginning of the buffer, and limit points to the end of the filled area:

ByteBuffer with position = 0, limit = 300,  capacity = 1000

With this, the buffer is ready to be read.

Reading from the ByteBuffer with get()

Let’s assume that the channel we want to write to can currently only take 200 of the 300 bytes. We can simulate this by supplying the ByteBuffer.get() method with a 200-byte-sized byte array in which the buffer should write its data:

buffer.get(new byte[200]);
Code language: Java (java)

printMetrics() now displays the following:

position = 200, limit = 300, capacity = 1000
Code language: plaintext (plaintext)

The read position has shifted to the right by 200 bytes – i.e., to the end of the data already read, which is equal to the beginning of the data that is not yet read:

ByteBuffer with position = 200, limit = 300,  capacity = 1000

Switching to Write Mode – How Not to Do It

To write back to the buffer now, you could make the following mistake: You set position to the end of the data, i.e., 300, and limit back to 1,000, which brings us back to precisely the state we were in after writing the ones and twos:

ByteBuffer with position = 300, limit = 1000,  capacity = 1000

Let’s assume that we would now write 300 more bytes into the buffer. The buffer would then look like this:

ByteBuffer with position = 600, limit = 1000,  capacity = 1000

If we would now use flip() to switch back to read mode, position would be back to 0:

ByteBuffer with position = 0, limit = 600,  capacity = 1000

Now, however, we would read the first 200 bytes, which we’ve already read, once more.

This approach is, therefore, wrong. The following section explains how to do it correctly.

Switching to Write Mode with Buffer.compact()

Instead, we must proceed as follows when switching to write mode:

  • We calculate the number of remaining bytes: remaining = limit - position. In the example, this results in 100.
  • We move the remaining bytes to the beginning of the buffer.
  • We set the write position to the end of the bytes shifted left. That’s 100 in the example.
  • We set limit to the end of the buffer.

ByteBuffer also provides a convenience method for this:

buffer.compact();
Code language: Java (java)

After invoking compact(), printMetrics() prints the following:

position = 100, limit = 1000, capacity = 1000
Code language: plaintext (plaintext)

In the graphic, the compact() process looks like this:

ByteBuffer with position = 100, limit = 1000,  capacity = 1000

The Next Cycle

Now we can write the next 300 bytes into the buffer:

byte[] threes = new byte[300]; Arrays.fill(threes, (byte) 3); buffer.put(threes);
Code language: Java (java)

printMetrics() now displays the following values:

position = 400, limit = 1000, capacity = 1000
Code language: plaintext (plaintext)

After writing the threes, position has shifted to the right by 300 bytes:

ByteBuffer with position =  400, limit = 1000, capacity = 1000

Now we can easily switch back to read mode using flip():

buffer.flip();
Code language: Java (java)

A final call to printMetrics() prints the following values:

position = 0, limit = 400, capacity = 1000
Code language: plaintext (plaintext)

The reading position is at the beginning of the buffer, to where the compact() method shifted the remaining 100 twos. So we can now continue reading at precisely the position where we stopped before.

ByteBuffer with position =  0, limit = 400, capacity = 1000

Summary

This article has explained the functionality of the Java ByteBuffer and its flip() and compact() methods with an example.

If this article has helped you understand ByteBuffer better, feel free to share it using one of the share buttons below, and leave me a comment.

Do you want to be informed when new articles are published on HappyCoders.eu? Then click here to sign up for the HappyCoders.eu newsletter.