Memra

ByteBuffer in practice

◈ 5 cards

allocate versus allocateDirect versus wrap, relative and absolute access, bulk transfers, typed put/get with byte order, and the views, duplicates and slices that share one store.

Three ways to obtain a buffer

ByteBuffer has no public constructor. Three static factories cover everything a network program needs.

ByteBuffer.allocate(n) returns an empty buffer of capacity n with position 0 and limit n. It is backed by a Java byte[] that array() and arrayOffset() will hand you. This is the input buffer — the object you pass to channel.read.

ByteBuffer.allocateDirect(n) exists on ByteBuffer and on no other buffer class. The VM is permitted — never required — to place the storage outside the Java heap, where the operating system can read into it and write out of it without an intermediate copy. array() on a direct buffer throws UnsupportedOperationException. Direct buffers can be meaningfully faster when they are large and long-lived, and they are distinctly more expensive to create, so they are a measured optimisation rather than a default.

ByteBuffer.wrap(bytes) wraps an array you already hold. That array is the backing store: a change on either side is visible on the other, and a wrapped buffer is never direct. This is the output buffer — you have bytes, you want them on the wire.

Relative and absolute access

Relative get() and put(b) act at position and move it. Absolute get(int index) and put(int index, byte b) act at the index you name and leave position alone — so no flip() is needed before reading back, and the order in which you make absolute puts is irrelevant. Absolute access at or past the limit throws IndexOutOfBoundsException; relative access past the limit throws BufferOverflowException on a put and BufferUnderflowException on a get. All three are unchecked.

Bulk transfers

Moving one byte at a time through a method call is the slow way. Each buffer class has bulk forms — for ByteBuffer those are get(byte[]), get(byte[], off, len), put(byte[]) and put(byte[], off, len). They start at position and advance it by the length moved. Note the asymmetry with streams: a bulk get that cannot be satisfied completely throws BufferUnderflowException instead of performing a short read, so test remaining() before you ask.

Typed data and byte order

ByteBuffer alone can read and write whole primitives: putInt, getInt, putShort, putLong, putFloat, putDouble and putChar, each in a relative and an absolute form. These are the NIO replacement for DataOutputStream and DataInputStream, with one power those never had — order(ByteOrder) chooses the interpretation. The default is ByteOrder.BIG_ENDIAN, which is also network byte order, so leave it alone unless a protocol specification says otherwise.

Views, duplicates and slices

asIntBuffer() and its five siblings overlay the same bytes with a typed view that begins at the current position and then keeps its own position, limit and mark. duplicate() gives a second, independent cursor over the same data, which is how one read-only response buffer serves many clients at once — each at its own offset, none waiting for the others. slice() is a duplicate restricted to the current position..limit window.

Worked example — a length-prefixed frame

Most binary protocols frame a message as a four-byte big-endian length followed by that many bytes of payload. Writing one is four buffer operations:

byte[] body = "PING 42".getBytes(StandardCharsets.UTF_8);
ByteBuffer frame = ByteBuffer.allocate(4 + body.length);
frame.putInt(body.length);
frame.put(body);
frame.flip();
while (frame.hasRemaining()) channel.write(frame);

putInt advances position by 4 and the bulk put by 7, so position is 11; flip() makes that the limit and returns position to 0; the loop drains it. Reading the frame back is the mirror image — getInt() for the length, then a bulk get into a right-sized array.

The subtle part is on the reading side. Several read() calls may be needed before all eleven bytes have arrived, and position is what makes that safe: you keep handing the channel the same buffer and it remembers how far you got. Only when position reaches 11 do you flip and parse.

ByteBuffer factorybacking arraydirectuse it forallocate(n)yes, array()noreading intoallocateDirect(n)no, throwsmaybelarge and measuredwrap(bytes)yes, sharedneverwriting outOnly allocateDirect may live off the Java heap.
allocate for reading into, wrap for writing out, allocateDirect only once a measurement says so. The middle row is the one that surprises people: array() on a direct buffer throws rather than returning a copy, so code that reaches for the backing array stops compiling against your intent the moment you switch factories.
NORMAL ~/memra/learn/comp-348/bytebuffer-in-practice utf-8 LF