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
, andcapacity
mean? - How to write into the
ByteBuffer
, how to read from it? - What exactly do the methods
flip()
andcompact()
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 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, aByteBuffer
is always in write mode, andlimit
is equal tocapacity
– 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 theallocate()
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:
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:
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:
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:
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:
Let’s assume that we would now write 300 more bytes into the buffer. The buffer would then look like this:
If we would now use flip()
to switch back to read mode, position
would be back to 0:
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:
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:
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.
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.