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.