Buffers
A Buffer in Java NIO is a fixed-capacity, typed container for data that serves as the intermediary between application code and channels. Every I/O operation in NIO reads data into a buffer or writes data from a buffer — there is no byte-at-a-time API. The Buffer class hierarchy provides typed buffers for every Java primitive: ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, and DoubleBuffer. ByteBuffer is the foundation type because channels always operate in bytes; the other types are views over a ByteBuffer or are used for in-memory data processing. Every buffer has three state variables — position, limit, and capacity — that control where reads and writes occur and how much data is available. The flip(), clear(), compact(), rewind(), and mark()/reset() operations manipulate these state variables to implement the write-then-read and read-then-write patterns that buffer-based I/O requires. ByteBuffer can be backed by a Java heap array (allocate()) or by off-heap native memory (allocateDirect()), with significant performance implications for I/O operations. This entry covers all buffer state variables and their transitions, every buffer method in depth, heap vs direct buffer performance model, view buffers and byte order, the slice() and duplicate() operations, and the common patterns for buffer-based channel I/O.
Buffer State — position, limit, capacity, and the Core Operations
// ── Buffer construction ───────────────────────────────────────────────
ByteBuffer heapBuf = ByteBuffer.allocate(1024); // heap, zeroed
ByteBuffer directBuf = ByteBuffer.allocateDirect(1024); // off-heap, zeroed
ByteBuffer wrappedBuf = ByteBuffer.wrap(new byte[]{1, 2, 3, 4, 5}); // existing array
System.out.println("Capacity: " + heapBuf.capacity()); // 1024
System.out.println("Limit: " + heapBuf.limit()); // 1024 (= capacity in write mode)
System.out.println("Position: " + heapBuf.position()); // 0
System.out.println("Remaining:" + heapBuf.remaining()); // 1024 (= limit - position)
// ── Write mode: position advances with each put() ────────────────────
ByteBuffer buf = ByteBuffer.allocate(16);
buf.putInt(42); // writes 4 bytes; position = 4
buf.putLong(1234567L); // writes 8 bytes; position = 12
buf.putInt(99); // writes 4 bytes; position = 16
System.out.println("After writes — position=" + buf.position() +
" limit=" + buf.limit()); // position=16, limit=16
// ── flip() — switch from write mode to read mode ──────────────────────
buf.flip();
// BEFORE flip: position=16, limit=16
// AFTER flip: position=0, limit=16 (limit was set to old position)
System.out.println("After flip — position=" + buf.position() +
" limit=" + buf.limit()); // position=0, limit=16
// Read back in same order:
int value1 = buf.getInt(); // 42; position = 4
long value2 = buf.getLong(); // 1234567L; position = 12
int value3 = buf.getInt(); // 99; position = 16
System.out.println(value1 + " " + value2 + " " + value3);
System.out.println("Remaining: " + buf.remaining()); // 0 — all consumed
// ── clear() — reset for overwriting ──────────────────────────────────
buf.clear();
// position=0, limit=16 (= capacity) — ready to write again
// Data is NOT erased — still in the backing array — just logically "undefined"
System.out.println("After clear — position=" + buf.position() +
" limit=" + buf.limit());
// ── compact() — preserve unread data, write more ─────────────────────
ByteBuffer partial = ByteBuffer.allocate(8);
partial.put(new byte[]{1, 2, 3, 4, 5, 6}); // write 6 bytes; position=6
partial.flip(); // flip: position=0, limit=6
partial.getInt(); // read 4 bytes; position=4
// Now: position=4, limit=6 — bytes [4,5] not yet read
partial.compact();
// compact copies remaining 2 bytes ([4,5] at indices 4,5) to indices 0,1
// position = 2 (number of bytes compacted)
// limit = capacity = 8
// Can now write 6 more bytes starting at position 2
System.out.println("After compact — position=" + partial.position() +
" limit=" + partial.limit()); // position=2, limit=8
// ── rewind() — re-read from start ────────────────────────────────────
ByteBuffer rewindBuf = ByteBuffer.allocate(8);
rewindBuf.putInt(111).putInt(222);
rewindBuf.flip();
System.out.println(rewindBuf.getInt()); // 111
rewindBuf.rewind(); // position=0, limit unchanged (4 wait — limit is 8)
// Actually flip() set limit=8 (position was 8 after two putInts)
// rewind() sets position=0
System.out.println(rewindBuf.getInt()); // 111 again
// ── mark() and reset() ────────────────────────────────────────────────
ByteBuffer markBuf = ByteBuffer.allocate(16);
markBuf.put(new byte[]{1,2,3,4,5,6,7,8}).flip();
markBuf.getInt(); // read 4 bytes; position=4
markBuf.mark(); // mark position 4
markBuf.getInt(); // read 4 more; position=8
markBuf.reset(); // restore position to 4
System.out.println(markBuf.getInt()); // 5 — re-reads the marked positionByteBuffer put/get Methods, Byte Order, and View Buffers
// ── Relative vs absolute get/put ─────────────────────────────────────
ByteBuffer buf = ByteBuffer.allocate(16);
// Relative put — advances position:
buf.put((byte) 10); // position: 0→1
buf.putShort((short) 256); // position: 1→3
buf.putInt(70000); // position: 3→7
buf.putLong(Long.MAX_VALUE); // position: 7→15
buf.put((byte) 42); // position: 15→16
buf.flip();
// Relative get — advances position:
byte b = buf.get(); // 10; position: 0→1
short s = buf.getShort(); // 256; position: 1→3
int i = buf.getInt(); // 70000; position: 3→7
// Absolute get — does NOT change position:
long lAtSeven = buf.getLong(7); // Long.MAX_VALUE; position stays at 7
long lRelative = buf.getLong(); // Long.MAX_VALUE; position: 7→15
System.out.printf("b=%d s=%d i=%d l=%d%n", b, s, i, lRelative);
// ── Byte order — critical for interoperability ────────────────────────
ByteBuffer data = ByteBuffer.allocate(8);
// Default: BIG_ENDIAN (Java standard, network byte order):
data.putInt(0x01020304); // bytes: [01, 02, 03, 04]
data.flip();
System.out.printf("Big-endian bytes: %02X %02X %02X %02X%n",
data.get(0), data.get(1), data.get(2), data.get(3));
// 01 02 03 04
data.clear();
// LITTLE_ENDIAN (x86 native, most Windows/Intel binary formats):
data.order(ByteOrder.LITTLE_ENDIAN);
data.putInt(0x01020304); // bytes: [04, 03, 02, 01]
data.flip();
System.out.printf("Little-endian bytes: %02X %02X %02X %02X%n",
data.get(0), data.get(1), data.get(2), data.get(3));
// 04 03 02 01
// Read a little-endian binary file:
try (FileChannel fc = FileChannel.open(Path.of("windows-format.bin"))) {
ByteBuffer le = ByteBuffer.allocateDirect(4096)
.order(ByteOrder.LITTLE_ENDIAN);
fc.read(le);
le.flip();
int windowsInt = le.getInt(); // correctly reads as little-endian
System.out.println("Windows int: " + windowsInt);
}
// ── View buffers ──────────────────────────────────────────────────────
ByteBuffer bytes = ByteBuffer.allocate(16).order(ByteOrder.BIG_ENDIAN);
bytes.putLong(0x0102030405060708L);
bytes.putLong(0x090A0B0C0D0E0F10L);
bytes.flip();
// IntBuffer view — reads 4-byte ints from the same memory:
IntBuffer ints = bytes.asIntBuffer(); // view of current position to limit
System.out.printf("As ints: %08X %08X %08X %08X%n",
ints.get(), ints.get(), ints.get(), ints.get());
// 01020304, 05060708, 090A0B0C, 0D0E0F10
// LongBuffer view:
bytes.position(0);
LongBuffer longs = bytes.asLongBuffer();
System.out.printf("As longs: %016X %016X%n", longs.get(), longs.get());
// 0102030405060708, 090A0B0C0D0E0F10
// ── slice() — independent sub-buffer of the same memory ───────────────
ByteBuffer original = ByteBuffer.allocate(16);
for (int k = 0; k < 16; k++) original.put((byte) k);
original.flip();
original.position(4);
original.limit(12);
ByteBuffer sliced = original.slice(); // covers bytes 4..11 of original
System.out.println("Sliced capacity: " + sliced.capacity()); // 8
System.out.println("Sliced[0]: " + sliced.get(0)); // 4 (offset in original)
sliced.put(0, (byte) 99); // modifies original too
System.out.println("Original[4]: " + original.get(4)); // 99
// ── duplicate() — independent position, shared data ───────────────────
ByteBuffer original2 = ByteBuffer.wrap(new byte[]{1,2,3,4,5,6,7,8});
ByteBuffer dup = original2.duplicate(); // same data, own position/limit
original2.getInt(); // advances original2's position to 4
System.out.println("Original pos: " + original2.position()); // 4
System.out.println("Dup pos: " + dup.position()); // 0 (independent)
dup.put(0, (byte) 99); // modifies original2's backing array too!
System.out.println("Original2[0]: " + original2.get(0)); // 99
// ── asReadOnlyBuffer() ────────────────────────────────────────────────
ByteBuffer writable = ByteBuffer.wrap(new byte[]{1,2,3,4});
ByteBuffer readOnly = writable.asReadOnlyBuffer();
readOnly.getInt(); // OK — reads 4 bytes
try {
readOnly.putInt(42); // ReadOnlyBufferException
} catch (ReadOnlyBufferException e) {
System.out.println("Cannot write to read-only buffer");
}Direct Buffers, Channel I/O Patterns, and Bulk Operations
// ── Direct buffer: when and why ──────────────────────────────────────
// Long-lived, reusable buffers for high-throughput I/O — use direct:
ByteBuffer ioBuffer = ByteBuffer.allocateDirect(65536); // 64KB direct buffer
// Allocate once, reuse for many read/write cycles
// Short-lived buffers or low-throughput code — heap is fine:
ByteBuffer parseBuffer = ByteBuffer.allocate(4096); // heap — fast allocation, GC-managed
// Checking buffer type:
System.out.println(ioBuffer.isDirect()); // true
System.out.println(parseBuffer.isDirect()); // false
// Direct buffer in channel I/O (no internal copy):
try (FileChannel fc = FileChannel.open(Path.of("data.bin"))) {
ByteBuffer direct = ByteBuffer.allocateDirect(65536);
int n = fc.read(direct); // JVM reads directly to native memory — no heap copy
direct.flip();
// Process direct.limit() bytes
}
// ── Standard read loop — the canonical NIO I/O pattern ───────────────
try (FileChannel fc = FileChannel.open(Path.of("data.bin"))) {
ByteBuffer buf = ByteBuffer.allocateDirect(8192);
while (true) {
buf.clear(); // 1. Switch to write mode (position=0, limit=capacity)
int n = fc.read(buf); // 2. Fill buffer from channel
if (n == -1) break; // 3. End of stream
buf.flip(); // 4. Switch to read mode (limit=n, position=0)
while (buf.hasRemaining()) { // 5. Process all bytes
byte b = buf.get();
process(b);
}
}
}
// ── compact() for partial records spanning multiple reads ─────────────
try (SocketChannel sc = SocketChannel.open(new InetSocketAddress("host", 8080))) {
ByteBuffer buf = ByteBuffer.allocateDirect(4096);
int recordSize = 16; // each record is 16 bytes
while (true) {
int n = sc.read(buf); // fill buffer
if (n == -1) break; // connection closed
buf.flip();
// Process as many complete records as available:
while (buf.remaining() >= recordSize) {
processRecord(buf); // reads exactly recordSize bytes
}
// compact() moves any partial record (< recordSize bytes) to buffer start:
buf.compact(); // position = bytes remaining (0 to recordSize-1)
// limit = capacity (ready for more data from channel)
}
}
// ── Standard write loop — ensure all bytes are written ────────────────
try (SocketChannel sc = SocketChannel.open(new InetSocketAddress("host", 80))) {
ByteBuffer request = ByteBuffer.wrap(
"GET / HTTP/1.0
".getBytes(StandardCharsets.US_ASCII));
// Write loop: channel.write() may not write all bytes in one call
while (request.hasRemaining()) {
sc.write(request); // writes from position to limit, advances position
}
// All bytes written when !request.hasRemaining() (position == limit)
}
// ── Bulk transfers between buffers ────────────────────────────────────
ByteBuffer src = ByteBuffer.wrap(new byte[]{1,2,3,4,5,6,7,8});
ByteBuffer dst = ByteBuffer.allocate(16);
// put(ByteBuffer) copies from src.position to src.limit into dst at dst.position:
dst.put(src); // copies 8 bytes; src.position=8, dst.position=8
// get into byte array:
src.flip();
byte[] array = new byte[src.remaining()];
src.get(array); // copies src[position..limit] into array
System.out.println(Arrays.toString(array)); // [1,2,3,4,5,6,7,8]
// ── hasArray() and array() — accessing backing array for heap buffers ──
ByteBuffer heapBuf2 = ByteBuffer.wrap(new byte[]{10, 20, 30, 40});
if (heapBuf2.hasArray()) {
byte[] backing = heapBuf2.array();
int offset = heapBuf2.arrayOffset(); // offset of position 0 in backing
// Safe to pass to API expecting byte[]:
legacyMethod(backing, offset, heapBuf2.limit());
}
// Direct buffer: hasArray() = false, array() throws UnsupportedOperationException