Java ByteBuffer
Last modified: July 16, 2026
This Java ByteBuffer tutorial shows how to use the java.nio.ByteBuffer
class to read, write, and manipulate byte data. We cover creating buffers,
reading and writing data, flipping, compacting, byte order, view buffers, and
reading files with channels.
ByteBuffer is essential in high-performance I/O and network
programming. It is used in file I/O with channels (NIO), socket communication,
binary protocol parsing, serialization frameworks, and usage of memory-mapped
files. Unlike traditional stream-based I/O, ByteBuffer enables
efficient bulk data transfer with minimal memory overhead, direct access to
underlying operating system memory via direct buffers, and fine-grained control
over position, limit, and capacity.
Typical use cases include reading and writing files with
FileChannel, implementing custom binary protocols over TCP
sockets, working with web servers and HTTP clients that use NIO
(e.g., Netty, Vert.x), encoding and decoding binary messages, and building
high-performance data processing pipelines where every byte copy matters.
Direct buffers are especially valuable for avoiding the overhead of copying
data between Java heap and native memory during I/O operations.
ByteBuffer
ByteBuffer is a class in the java.nio package that
provides a buffer for bytes. It is a key component of Java's New I/O (NIO) API.
A ByteBuffer is essentially a container for a linear, finite
sequence of bytes, with three key properties that define its state:
| Property | Description |
|---|---|
| Capacity | The total number of bytes the buffer can hold. Set at creation and never changes. |
| Position | The index of the next byte to be read or written. Starts at 0 and advances automatically. |
| Limit | The index of the first byte that should not be read or written. In write mode, limit equals capacity; after flip, limit is set to the old position (the amount of data written). |
The relationship between these properties is always:
0 ≤ position ≤ limit ≤ capacity.
Creating buffers
ByteBuffer instances are created via static factory methods rather
than public constructors.
// Heap buffer
ByteBuffer buf = ByteBuffer.allocate(1024);
// Direct buffer (off-heap)
ByteBuffer bufDirect = ByteBuffer.allocateDirect(1024);
// Wrap existing array
byte[] data = {1, 2, 3, 4, 5};
ByteBuffer bufWrapped = ByteBuffer.wrap(data);
allocate(int capacity) creates a heap buffer backed by a
byte[] array. allocateDirect creates a direct buffer
that uses memory outside the Java heap, which can be more efficient for I/O
operations. wrap(byte[]) creates a buffer backed by an existing
byte array; changes to the buffer are reflected in the array and vice versa.
Writing and reading bytes
Data is written to a buffer with put methods and read from it with
get methods. After writing, the buffer must be flipped
before reading: flip sets the limit to the current position and
resets the position to zero.
import java.nio.ByteBuffer;
void main() {
ByteBuffer buf = ByteBuffer.allocate(10);
buf.put((byte) 10);
buf.put((byte) 20);
buf.put((byte) 30);
buf.flip();
while (buf.hasRemaining()) {
System.out.println(buf.get());
}
}
We allocate a 10-byte buffer, write three bytes, flip it, and read them back.
buf.put((byte) 10); buf.put((byte) 20); buf.put((byte) 30);
Three bytes are written to the buffer. After these writes, the position is 3 and the limit is 10 (the capacity).
buf.flip();
flip sets the limit to the current position (3) and resets the
position to 0, preparing the buffer for reading.
while (buf.hasRemaining()) {
System.out.println(buf.get());
}
The hasRemaining method returns true while the
position is less than the limit. Each get reads one byte and
advances the position.
$ java Main.java 10 20 30
Writing with absolute and relative positions
Both put and get have absolute overloads that
operate at a specific index without affecting the buffer's position.
import java.nio.ByteBuffer;
void main() {
ByteBuffer buf = ByteBuffer.allocate(8);
// Relative puts
buf.put((byte) 1);
buf.put((byte) 2);
// Absolute put at index 7
buf.put(7, (byte) 99);
buf.flip();
while (buf.hasRemaining()) {
System.out.println(buf.get());
}
}
The relative put writes at the current position and advances it.
The absolute put(int index, byte b) writes at a specific index
without changing the position.
$ java Main.java 1 2 0 0 0 0 0 99
Bulk put and get
A ByteBuffer can transfer multiple bytes at once using the bulk
put(byte[]) and get(byte[]) methods.
import java.nio.ByteBuffer;
import java.util.Arrays;
void main() {
byte[] input = {10, 20, 30, 40, 50};
ByteBuffer buf = ByteBuffer.allocate(16);
buf.put(input);
buf.flip();
byte[] output = new byte[buf.remaining()];
buf.get(output);
System.out.println(Arrays.toString(output));
}
The bulk put(byte[]) copies the entire source array into the
buffer starting at the current position. The bulk get(byte[]) reads
output.length bytes from the buffer into the destination array.
$ java Main.java [10, 20, 30, 40, 50]
Compact
The compact method compacts a buffer by copying unread bytes to
the beginning of the buffer. This is useful when you want to continue writing
to a buffer that still has some unread data.
import java.nio.ByteBuffer;
import java.util.Arrays;
void main() {
ByteBuffer buf = ByteBuffer.allocate(10);
buf.put(new byte[]{1, 2, 3, 4, 5, 6, 7, 8});
buf.flip();
// Read first three bytes
byte[] dst = new byte[3];
buf.get(dst);
System.out.println("Read: " + Arrays.toString(dst));
// Compact: move remaining bytes to front
buf.compact();
// Write more data
buf.put(new byte[]{9, 10});
buf.flip();
byte[] out = new byte[buf.remaining()];
buf.get(out);
System.out.println("Remaining: " + Arrays.toString(out));
}
After reading three bytes, five unread bytes remain. compact shifts
them to the front of the buffer, sets position to 5 (the amount of data
remaining), and sets limit to capacity. Then we write two more bytes and flip
for reading.
$ java Main.java Read: [1, 2, 3] Remaining: [4, 5, 6, 7, 8, 9, 10]
Converting between strings and ByteBuffers
A ByteBuffer can be converted to and from strings using a
Charset.
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
void main() {
String msg = "Hello, Java!";
// Encode string to ByteBuffer
ByteBuffer buf = StandardCharsets.UTF_8.encode(msg);
// Decode ByteBuffer to string
String decoded = StandardCharsets.UTF_8.decode(buf).toString();
System.out.println(decoded);
// Using wrap and Charset
byte[] data = msg.getBytes(StandardCharsets.UTF_8);
ByteBuffer buf2 = ByteBuffer.wrap(data);
String decoded2 = StandardCharsets.UTF_8.decode(buf2).toString();
System.out.println(decoded2);
}
Charset.encode encodes a string directly into a
ByteBuffer. Charset.decode decodes a
ByteBuffer back to a string.
$ java Main.java Hello, Java! Hello, Java!
Byte order
The order method sets the byte order of a buffer, which affects
how multi-byte values (short, int, long)
are read and written. The two orders are BIG_ENDIAN (default) and
LITTLE_ENDIAN.
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
void main() {
ByteBuffer buf = ByteBuffer.allocate(8);
buf.order(ByteOrder.BIG_ENDIAN);
buf.putInt(0x12345678);
buf.flip();
System.out.printf("Big endian: 0x%08x%n", buf.getInt());
buf.clear();
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.putInt(0x12345678);
buf.flip();
System.out.printf("Little endian: 0x%08x%n", buf.getInt());
}
The same integer value is written and read using different byte orders. With big endian, the most significant byte is stored first; with little endian, the least significant byte is stored first.
$ java Main.java Big endian: 0x12345678 Little endian: 0x78563412
View buffers
A ByteBuffer can create view buffers that interpret the
byte data as a specific primitive type. Changes to the view buffer are reflected
in the backing buffer and vice versa.
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
void main() {
ByteBuffer byteBuf = ByteBuffer.allocate(16);
byteBuf.putInt(100);
byteBuf.putInt(200);
byteBuf.putInt(300);
byteBuf.putInt(400);
byteBuf.flip();
IntBuffer intBuf = byteBuf.asIntBuffer();
while (intBuf.hasRemaining()) {
System.out.println(intBuf.get());
}
}
We write four integers into a ByteBuffer, then create an
IntBuffer view that reads them as a sequence of ints.
IntBuffer intBuf = byteBuf.asIntBuffer();
The asIntBuffer method returns an IntBuffer backed by
the byte buffer. Other view methods include asShortBuffer,
asCharBuffer, asLongBuffer, asFloatBuffer,
and asDoubleBuffer.
$ java Main.java 100 200 300 400
Reading a file with FileChannel
One of the most common uses of ByteBuffer is reading files via a
FileChannel. The channel reads bytes directly into the buffer.
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
void main() {
var path = Path.of("test.txt");
try (var channel = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buf);
if (bytesRead != -1) {
buf.flip();
var data = new byte[buf.remaining()];
buf.get(data);
System.out.println(new String(data));
}
}
}
We open a FileChannel for reading, allocate a buffer, and call
channel.read(buf) to read bytes from the file into the buffer.
After reading, we flip the buffer and extract the bytes.
$ java Main.java Hello there!
For larger files, we would loop over channel.read until it returns
-1, compacting or clearing the buffer between reads.
Mark and reset
The buffer's position can be saved with mark and restored with
reset. This is useful for looking ahead in the data.
import java.nio.ByteBuffer;
void main() {
ByteBuffer buf = ByteBuffer.wrap(new byte[]{1, 2, 3, 4, 5});
System.out.println(buf.get()); // 1
System.out.println(buf.get()); // 2
buf.mark(); // mark at position 2
System.out.println(buf.get()); // 3
System.out.println(buf.get()); // 4
buf.reset(); // go back to position 2
System.out.println(buf.get()); // 3 again
}
After reading two bytes, we mark the position. After reading two more bytes, we reset to the mark, allowing us to re-read the data.
$ java Main.java 1 2 3 4 3
Buffer rewind and clear
The rewind method resets the position to 0 without changing the
limit, useful for re-reading a buffer. The clear method resets both
position to 0 and limit to capacity, preparing the buffer for a fresh write.
import java.nio.ByteBuffer;
void main() {
ByteBuffer buf = ByteBuffer.wrap(new byte[]{10, 20, 30});
System.out.println(buf.get()); // 10
System.out.println(buf.get()); // 20
buf.rewind();
System.out.println("After rewind:");
while (buf.hasRemaining()) {
System.out.println(buf.get());
}
buf.clear();
System.out.println("After clear, position: " + buf.position()
+ ", limit: " + buf.limit());
}
rewind lets us re-read all three bytes from the beginning.
clear resets position to 0 and limit to capacity for new writes.
$ java Main.java 10 20 After rewind: 10 20 30 After clear, position: 0, limit: 3
Source
Java ByteBuffer - Language Reference
In this article, we have worked with Java ByteBuffer.
Author
List all Java tutorials.