15.1 Buffer Fundamentals
Master the ByteBuffer interface for efficient data handling in NIO operations.
Buffer Basics
Buffer States:
import java.nio.ByteBuffer;
// Buffer states: position, limit, capacity, mark
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Initial state
System.out.println("Position: " + buffer.position()); // 0
System.out.println("Limit: " + buffer.limit()); // 1024
System.out.println("Capacity: " + buffer.capacity()); // 1024
System.out.println("Remaining: " + buffer.remaining()); // 1024
// After writing data
buffer.put((byte) 0x48); // 'H'
buffer.put((byte) 0x65); // 'e'
buffer.put((byte) 0x6C); // 'l'
buffer.put((byte) 0x6C); // 'l'
buffer.put((byte) 0x6F); // 'o'
System.out.println("Position after writes: " + buffer.position()); // 5
System.out.println("Remaining: " + buffer.remaining()); // 1019
Buffer Lifecycle:
// 1. Create and write (writing mode)
ByteBuffer buffer = ByteBuffer.allocate(128);
buffer.put("Hello, World!".getBytes());
System.out.println("Position: " + buffer.position()); // 13
// 2. Flip for reading (reading mode)
buffer.flip();
System.out.println("Position: " + buffer.position()); // 0
System.out.println("Limit: " + buffer.limit()); // 13
// 3. Read data
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println("String: " + new String(data)); // "Hello, World!"
// 4. Clear for reuse (back to writing mode)
buffer.clear();
System.out.println("Position: " + buffer.position()); // 0
System.out.println("Limit: " + buffer.limit()); // 128
Buffer Allocation
Heap Buffers:
// Allocate on heap (JVM managed)
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
System.out.println("Is direct: " + heapBuffer.isDirect()); // false
// Access underlying array
if (heapBuffer.hasArray()) {
byte[] array = heapBuffer.array();
int offset = heapBuffer.arrayOffset();
System.out.println("Array: " + array);
System.out.println("Offset: " + offset);
}
// Wrap existing array
byte[] data = new byte[]{0x01, 0x02, 0x03, 0x04};
ByteBuffer wrappedBuffer = ByteBuffer.wrap(data);
wrappedBuffer.get(); // 0x01
wrappedBuffer.get(); // 0x02
Direct Buffers:
// Allocate off-heap (native memory)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
System.out.println("Is direct: " + directBuffer.isDirect()); // true
// Direct buffers are more efficient for native I/O
// but have overhead in creation and GC pressure
// Use with channels
try (FileChannel channel = FileChannel.open(Path.of("file.bin"),
StandardOpenOption.READ)) {
// Direct buffer preferred for channel operations
ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
while (channel.read(buffer) > 0) {
buffer.flip();
// Process buffer
buffer.clear();
}
}
Capacity Comparisons:
ByteBuffer heap = ByteBuffer.allocate(1024);
ByteBuffer direct = ByteBuffer.allocateDirect(1024);
// Both have same capacity
System.out.println("Heap capacity: " + heap.capacity()); // 1024
System.out.println("Direct capacity: " + direct.capacity()); // 1024
// Heap can access array
System.out.println("Heap has array: " + heap.hasArray()); // true
System.out.println("Direct has array: " + direct.hasArray()); // false
// Performance implications
long start = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
heap.putInt(0, i);
}
long heapTime = System.nanoTime() - start;
start = System.nanoTime();
for (int i = 0; i < 1000000; i++) {
direct.putInt(0, i);
}
long directTime = System.nanoTime() - start;
System.out.println("Heap time: " + heapTime);
System.out.println("Direct time: " + directTime);
Buffer Operations
Positioning Methods:
ByteBuffer buffer = ByteBuffer.allocate(256);
// Write some data
buffer.put("Position test".getBytes());
System.out.println("Position: " + buffer.position()); // 14
// Get/set position
buffer.position(0); // Reset to beginning
System.out.println("Data: " + (char) buffer.get()); // 'P'
// Mark and reset
buffer.position(5);
buffer.mark();
buffer.position(10);
System.out.println("Position: " + buffer.position()); // 10
buffer.reset();
System.out.println("Position after reset: " + buffer.position()); // 5
// Rewind (reset to beginning)
buffer.rewind();
System.out.println("Position after rewind: " + buffer.position()); // 0
// Clear (reset to beginning, set limit to capacity)
buffer.clear();
System.out.println("Position: " + buffer.position()); // 0
System.out.println("Limit: " + buffer.limit()); // 256
// Flip (prepare for reading)
buffer.put("test".getBytes());
buffer.flip();
System.out.println("Position: " + buffer.position()); // 0
System.out.println("Limit: " + buffer.limit()); // 4
// Compact (discard read data, keep unread)
buffer.get(); // Read one byte
buffer.compact();
// Position now at end of remaining data
Reading Data:
ByteBuffer buffer = ByteBuffer.wrap(new byte[]{0x01, 0x02, 0x03, 0x04, 0x05});
// Read single byte
byte b1 = buffer.get(); // 0x01
byte b2 = buffer.get(); // 0x02
// Read into array
byte[] data = new byte[3];
buffer.get(data); // Reads 0x03, 0x04, 0x05
System.out.println("Array: " + Arrays.toString(data));
// Read at position
buffer.position(0);
byte b = buffer.get(2); // 0x03 without changing position
// Read all
buffer.position(0);
byte[] all = new byte[buffer.remaining()];
buffer.get(all);
Writing Data:
ByteBuffer buffer = ByteBuffer.allocate(256);
// Write single byte
buffer.put((byte) 0x48);
// Write array
buffer.put(new byte[]{0x65, 0x6C, 0x6C, 0x6F});
// Write string bytes
buffer.put("World".getBytes());
// Write at position (doesn't change current position)
buffer.put(0, (byte) 0xFF);
// Write from buffer
ByteBuffer source = ByteBuffer.wrap(new byte[]{0x01, 0x02, 0x03});
buffer.put(source); // Copies remaining bytes from source
// Check before writing
if (buffer.remaining() >= 4) {
buffer.putInt(0x12345678);
}
Typed Buffers
Primitive Type Buffers:
// IntBuffer
IntBuffer intBuffer = IntBuffer.allocate(256);
intBuffer.put(42);
intBuffer.put(100);
intBuffer.put(255);
intBuffer.flip();
System.out.println("First int: " + intBuffer.get()); // 42
// LongBuffer
LongBuffer longBuffer = LongBuffer.allocate(256);
longBuffer.put(System.currentTimeMillis());
longBuffer.put(Long.MAX_VALUE);
// DoubleBuffer
DoubleBuffer doubleBuffer = DoubleBuffer.allocate(256);
doubleBuffer.put(3.14159);
doubleBuffer.put(2.71828);
doubleBuffer.flip();
while (doubleBuffer.hasRemaining()) {
System.out.println(doubleBuffer.get());
}
// FloatBuffer
FloatBuffer floatBuffer = FloatBuffer.allocate(256);
floatBuffer.put(1.5f);
floatBuffer.put(2.5f);
// CharBuffer
CharBuffer charBuffer = CharBuffer.allocate(256);
charBuffer.put('H');
charBuffer.put('e');
charBuffer.put('l');
charBuffer.put('l');
charBuffer.put('o');
charBuffer.flip();
System.out.println("Chars: " + charBuffer); // "Hello"
Working with Views:
ByteBuffer byteBuffer = ByteBuffer.allocate(256);
// Create typed views of the same underlying data
IntBuffer intView = byteBuffer.asIntBuffer();
LongBuffer longView = byteBuffer.asLongBuffer();
DoubleBuffer doubleView = byteBuffer.asDoubleBuffer();
// Write through IntBuffer view
intView.put(0, 0x12345678);
intView.put(1, 0x9ABCDEF0);
// Read through ByteBuffer
byteBuffer.position(0);
for (int i = 0; i < 8; i++) {
System.out.printf("%02X ", byteBuffer.get());
}
System.out.println();
// Position is shared between original and view
byteBuffer.position(4);
System.out.println("Position in intView: " + intView.position()); // 1
// Views are direct references (changes affect original)
intView.put(1, 0xFFFFFFFF);
System.out.println("Original position 4-8: ");
byteBuffer.position(4);
for (int i = 0; i < 4; i++) {
System.out.printf("%02X ", byteBuffer.get());
}
Byte Order
Endianness:
ByteBuffer buffer = ByteBuffer.allocate(256);
// Check default byte order
System.out.println("Default byte order: " + buffer.order());
// BIG_ENDIAN on most systems
// Get current byte order
ByteOrder order = buffer.order();
// Set byte order
buffer.order(ByteOrder.LITTLE_ENDIAN);
System.out.println("Order set to: " + buffer.order());
// Multi-byte values respect byte order
buffer.putInt(0x12345678);
buffer.flip();
System.out.println("Big-endian bytes:");
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.position(0);
for (int i = 0; i < 4; i++) {
System.out.printf("%02X ", buffer.get());
}
System.out.println();
System.out.println("Little-endian bytes:");
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.position(0);
for (int i = 0; i < 4; i++) {
System.out.printf("%02X ", buffer.get());
}
Handling Network Data:
/**
* Convert between network byte order and system byte order
*/
public class NetworkByteOrder {
/**
* Read network (big-endian) integer
*/
public static int readNetworkInt(ByteBuffer buffer) {
ByteOrder original = buffer.order();
try {
buffer.order(ByteOrder.BIG_ENDIAN);
return buffer.getInt();
} finally {
buffer.order(original);
}
}
/**
* Write network (big-endian) integer
*/
public static void writeNetworkInt(ByteBuffer buffer, int value) {
ByteOrder original = buffer.order();
try {
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.putInt(value);
} finally {
buffer.order(original);
}
}
/**
* Read multi-byte value with explicit byte order
*/
public static long readValue(ByteBuffer buffer, int bytes, ByteOrder order) {
long value = 0;
for (int i = 0; i < bytes; i++) {
byte b = buffer.get();
if (order == ByteOrder.BIG_ENDIAN) {
value = (value << 8) | (b & 0xFF);
} else {
value |= ((long) (b & 0xFF)) << (i * 8);
}
}
return value;
}
}
Buffer Comparison and Equality
Comparing Buffers:
ByteBuffer buf1 = ByteBuffer.wrap(new byte[]{0x01, 0x02, 0x03});
ByteBuffer buf2 = ByteBuffer.wrap(new byte[]{0x01, 0x02, 0x03});
ByteBuffer buf3 = ByteBuffer.wrap(new byte[]{0x01, 0x02, 0x04});
// Equality (contents from position to limit)
System.out.println("buf1 equals buf2: " + buf1.equals(buf2)); // true
System.out.println("buf1 equals buf3: " + buf1.equals(buf3)); // false
// Comparison (like compareTo for strings)
System.out.println("buf1 compareTo buf3: " + buf1.compareTo(buf3)); // negative
// Manual comparison
boolean contentsEqual = true;
buf1.rewind();
buf2.rewind();
while (buf1.hasRemaining() && buf2.hasRemaining()) {
if (buf1.get() != buf2.get()) {
contentsEqual = false;
break;
}
}
if (buf1.hasRemaining() || buf2.hasRemaining()) {
contentsEqual = false;
}
System.out.println("Contents equal: " + contentsEqual);
Real-World Example: BufferPool
import java.nio.ByteBuffer;
import java.util.Queue;
import java.util.concurrent.*;
/**
* Object pool for reusing buffers
*/
public class ByteBufferPool {
private final Queue<ByteBuffer> pool;
private final int bufferSize;
private final int poolSize;
private final boolean direct;
public ByteBufferPool(int bufferSize, int poolSize, boolean direct) {
this.bufferSize = bufferSize;
this.poolSize = poolSize;
this.direct = direct;
this.pool = new ConcurrentLinkedQueue<>();
// Pre-allocate buffers
for (int i = 0; i < poolSize; i++) {
pool.offer(allocateBuffer());
}
}
/**
* Get buffer from pool or allocate new
*/
public ByteBuffer acquire() {
ByteBuffer buffer = pool.poll();
if (buffer == null) {
buffer = allocateBuffer();
}
return buffer.clear();
}
/**
* Return buffer to pool
*/
public void release(ByteBuffer buffer) {
if (buffer != null && buffer.capacity() == bufferSize) {
buffer.clear();
pool.offer(buffer);
}
}
/**
* Allocate buffer
*/
private ByteBuffer allocateBuffer() {
return direct
? ByteBuffer.allocateDirect(bufferSize)
: ByteBuffer.allocate(bufferSize);
}
/**
* Get pool statistics
*/
public int getAvailableCount() {
return pool.size();
}
/**
* Clear all buffers
*/
public void clear() {
pool.clear();
}
/**
* Usage example
*/
public static void main(String[] args) {
ByteBufferPool pool = new ByteBufferPool(8192, 10, true);
// Acquire buffer
ByteBuffer buffer = pool.acquire();
System.out.println("Available buffers: " + pool.getAvailableCount()); // 9
try {
// Use buffer
buffer.put("Hello, World!".getBytes());
buffer.flip();
System.out.println("Data: " + new String(buffer.array(), 0, buffer.limit()));
} finally {
// Return to pool
pool.release(buffer);
System.out.println("Available buffers after release: " + pool.getAvailableCount()); // 10
}
// Acquire multiple buffers
ByteBuffer[] buffers = new ByteBuffer[5];
for (int i = 0; i < 5; i++) {
buffers[i] = pool.acquire();
buffers[i].putInt(i * 100);
}
System.out.println("Available after acquiring 5: " + pool.getAvailableCount()); // 5
// Return all
for (ByteBuffer buf : buffers) {
pool.release(buf);
}
System.out.println("Available after returning all: " + pool.getAvailableCount()); // 10
}
}
Buffer Utilities:
public class BufferUtils {
/**
* Copy buffer contents
*/
public static ByteBuffer copy(ByteBuffer source) {
ByteBuffer copy = source.isDirect()
? ByteBuffer.allocateDirect(source.capacity())
: ByteBuffer.allocate(source.capacity());
copy.order(source.order());
source.mark();
source.rewind();
copy.put(source);
source.reset();
return copy.flip();
}
/**
* Print buffer contents as hex
*/
public static String toHexString(ByteBuffer buffer) {
StringBuilder sb = new StringBuilder();
int pos = buffer.position();
while (buffer.hasRemaining()) {
sb.append(String.format("%02X ", buffer.get()));
}
buffer.position(pos);
return sb.toString();
}
/**
* Create buffer from hex string
*/
public static ByteBuffer fromHexString(String hex) {
String[] parts = hex.split(" ");
ByteBuffer buffer = ByteBuffer.allocate(parts.length);
for (String part : parts) {
buffer.put((byte) Integer.parseInt(part, 16));
}
return buffer.flip();
}
/**
* Fill buffer with pattern
*/
public static void fill(ByteBuffer buffer, byte pattern) {
while (buffer.hasRemaining()) {
buffer.put(pattern);
}
buffer.flip();
}
/**
* Ensure buffer has space for more bytes
*/
public static ByteBuffer ensureCapacity(ByteBuffer buffer, int additionalBytes) {
if (buffer.remaining() < additionalBytes) {
ByteBuffer newBuffer = buffer.isDirect()
? ByteBuffer.allocateDirect(buffer.capacity() * 2)
: ByteBuffer.allocate(buffer.capacity() * 2);
newBuffer.order(buffer.order());
buffer.flip();
newBuffer.put(buffer);
return newBuffer;
}
return buffer;
}
}
Best Practices
1. Choose Correct Buffer Type:
// For channel I/O - use direct
ByteBuffer direct = ByteBuffer.allocateDirect(8192);
// For simple operations - use heap
ByteBuffer heap = ByteBuffer.allocate(1024);
// For structured data - use typed buffers
IntBuffer ints = IntBuffer.allocate(256);
2. Manage Lifecycle Carefully:
// Always follow this pattern
ByteBuffer buffer = ByteBuffer.allocate(1024);
// ... write data ...
buffer.flip(); // Switch to reading
// ... read data ...
buffer.clear(); // Reset for reuse
3. Handle Byte Order Explicitly:
// When working with network data, be explicit
buffer.order(ByteOrder.BIG_ENDIAN);
int networkValue = buffer.getInt();
4. Use Pools for High-Throughput:
// Reuse buffers instead of creating new ones
ByteBuffer buffer = pool.acquire();
try {
// Use buffer
} finally {
pool.release(buffer);
}
5. Verify Capacity Before Operations:
// Always check space before writing
if (buffer.remaining() >= bytesNeeded) {
buffer.putInt(value);
}
These buffer fundamentals form the foundation for efficient NIO operations in modern Java applications.