ZetCode

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.

Main.java
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.

Main.java
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.

Main.java
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.

Main.java
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.

Main.java
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.

Main.java
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.

Main.java
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.

Main.java
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.

Main.java
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.

Main.java
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Java tutorials.