Skip to main content
  1. Java NIO (New I/O)/

Buffer

10 mins

The Buffer class is the abstract base class for all NIO buffers, providing the core functionality for position, limit, and capacity management.

Source Code #

View Source on GitHub

Core Implementation #

public abstract class Buffer {
    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
    
    // Creates a new buffer with the given mark, position, limit, and capacity
    Buffer(int mark, int pos, int lim, int cap) {
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);
        this.capacity = cap;
        limit(lim);
        position(pos);
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("Mark > position: (" + mark + " > " + pos + ")");
            this.mark = mark;
        }
    }
}

Implementation Details #

Core State Management #

// Returns this buffer's capacity
public final int capacity() {
    return capacity;
}

// Returns this buffer's position
public final int position() {
    return position;
}

// Sets this buffer's position
public final Buffer position(int newPosition) {
    if ((newPosition > limit) || (newPosition < 0))
        throw new IllegalArgumentException();
    position = newPosition;
    if (mark > position) mark = -1;
    return this;
}

// Returns this buffer's limit
public final int limit() {
    return limit;
}

// Sets this buffer's limit
public final Buffer limit(int newLimit) {
    if ((newLimit > capacity) || (newLimit < 0))
        throw new IllegalArgumentException();
    limit = newLimit;
    if (position > limit) position = limit;
    if (mark > limit) mark = -1;
    return this;
}

// Sets this buffer's mark at its position
public final Buffer mark() {
    mark = position;
    return this;
}

// Resets this buffer's position to the previously-marked position
public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}

// Clears this buffer (position=0, limit=capacity, mark=-1)
public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

// Flips this buffer (limit=position, position=0, mark=-1)
public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}

// Rewinds this buffer (position=0, mark=-1)
public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}

// Returns the number of elements between the current position and the limit
public final int remaining() {
    return limit - position;
}

// Tells whether there are any elements between the current position and the limit
public final boolean hasRemaining() {
    return position < limit;
}

// Tells whether this buffer is read-only
public abstract boolean isReadOnly();

Invariant Management #

The Buffer class maintains these invariants:

mark <= position <= limit <= capacity

All operations that modify these values enforce the invariants:

// Example from position() method
public final Buffer position(int newPosition) {
    if ((newPosition > limit) || (newPosition < 0))
        throw new IllegalArgumentException();
    position = newPosition;
    if (mark > position) mark = -1;  // Invalidate mark if beyond new position
    return this;
}

Buffer Comparison #

// Compares this buffer to another object
public int compareTo(Buffer other) {
    int n = this.position() - other.position();
    if (n != 0)
        return n;
    n = this.limit() - other.limit();
    if (n != 0)
        return n;
    n = this.capacity() - other.capacity();
    if (n != 0)
        return n;
    return 0;
}

// Tells whether or not this buffer is equal to another object
public boolean equals(Object ob) {
    if (this == ob)
        return true;
    if (!(ob instanceof Buffer))
        return false;
    Buffer that = (Buffer)ob;
    if (this.remaining() != that.remaining())
        return false;
    
    int p = this.position();
    for (int i = 0; i < this.remaining(); i++)
        if (!equals(this.get(p + i), that.get(that.position() + i)))
            return false;
    
    return true;
}

// Abstract equals method for element comparison
abstract boolean equals(Object ob1, Object ob2);

Hash Code Calculation #

// Computes the hash code of this buffer
public int hashCode() {
    int h = 1;
    int p = position;
    for (int i = limit - 1; i >= p; i--)
        h = h * 31 + hashCode(get(i));
    return h;
}

// Abstract hashCode method for element hashing
abstract int hashCode(Object obj);

String Representation #

// Returns a string summarizing the state of this buffer
public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append(getClass().getName());
    sb.append("[pos=");
    sb.append(position());
    sb.append(" lim=");
    sb.append(limit());
    sb.append(" cap=");
    sb.append(capacity());
    sb.append("]");
    return sb.toString();
}

Buffer Operations #

Core Operations #

  1. clear(): Prepares buffer for writing

    • position = 0, limit = capacity, mark = -1
  2. flip(): Prepares buffer for reading

    • limit = position, position = 0, mark = -1
  3. rewind(): Prepares for re-reading

    • position = 0, mark = -1 (limit unchanged)
  4. mark(): Marks current position

    • mark = position
  5. reset(): Returns to marked position

    • position = mark

Data Transfer #

// Relative get operation
public abstract Object get();

// Absolute get operation
public abstract Object get(int index);

// Relative put operation
public abstract Buffer put(Object o);

// Absolute put operation
public abstract Buffer put(int index, Object o);

Bulk Operations #

// Relative bulk get operation
public Buffer get(Object[] dst, int offset, int length) {
    checkBounds(offset, length, dst.length);
    if (length > remaining())
        throw new BufferUnderflowException();
    for (int i = offset; i < offset + length; i++)
        dst[i] = get();
    return this;
}

// Relative bulk put operation
public final Buffer put(Object[] src, int offset, int length) {
    checkBounds(offset, length, src.length);
    if (length > remaining())
        throw new BufferOverflowException();
    for (int i = offset; i < offset + length; i++)
        put(src[i]);
    return this;
}

// Checks if the given bounds are valid
private static void checkBounds(int off, int len, int size) {
    if ((off | len | (off + len) | (size - (off + len))) < 0)
        throw new IndexOutOfBoundsException();
}

Concrete Buffer Implementations #

ByteBuffer #

The most commonly used buffer implementation:

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
    // Factory methods
    public static ByteBuffer allocate(int capacity) {
        return new HeapByteBuffer(capacity, capacity);
    }
    
    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
    
    public static ByteBuffer wrap(byte[] array) {
        return new HeapByteBuffer(array, 0, array.length);
    }
    
    public static ByteBuffer wrap(byte[] array, int offset, int length) {
        return new HeapByteBuffer(array, offset, length);
    }
}

CharBuffer #

Character buffer for text processing:

public abstract class CharBuffer extends Buffer implements CharSequence, Comparable<CharBuffer> {
    // Factory methods
    public static CharBuffer allocate(int capacity) {
        return new HeapCharBuffer(capacity, capacity);
    }
    
    public static CharBuffer wrap(CharSequence csq) {
        return wrap(csq.toString());
    }
    
    public static CharBuffer wrap(char[] array) {
        return new HeapCharBuffer(array, 0, array.length);
    }
    
    public static CharBuffer wrap(char[] array, int offset, int length) {
        return new HeapCharBuffer(array, offset, length);
    }
}

View Buffers #

Buffers that provide views of byte buffers as other primitive types:

// IntBuffer - views byte buffer as int sequence
public abstract class IntBuffer extends Buffer implements Comparable<IntBuffer> {
    // Factory methods
    public static IntBuffer allocate(int capacity) {
        return new HeapIntBuffer(capacity, capacity);
    }
    
    public static IntBuffer wrap(int[] array) {
        return new HeapIntBuffer(array, 0, array.length);
    }
}

// Similar for LongBuffer, FloatBuffer, DoubleBuffer, ShortBuffer

Buffer Allocation Strategies #

Heap Buffers #

// Allocated in JVM heap, subject to GC
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);

// Characteristics:
// - Allocated in JVM heap memory
// - Subject to garbage collection
// - Faster allocation
// - Slower I/O operations (requires copying to native memory)

Direct Buffers #

// Allocated in native memory, outside JVM heap
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

// Characteristics:
// - Allocated in native memory (outside JVM heap)
// - Not subject to garbage collection
// - Slower allocation
// - Faster I/O operations (zero-copy)
// - Requires explicit cleanup (uses Cleaner/PhantomReference)

Wrapped Buffers #

// Wraps existing arrays
byte[] array = new byte[1024];
ByteBuffer wrappedBuffer = ByteBuffer.wrap(array);

// Characteristics:
// - Shares the underlying array
// - Changes to buffer affect the array and vice versa
// - No memory copying overhead

Performance Considerations #

Buffer Type Selection #

Buffer TypeAllocation SpeedI/O SpeedMemory OverheadGC Impact
HeapFastSlowerLowHigh
DirectSlowFastHighNone
WrappedN/AMediumNoneMedium

When to Use Each Type #

Heap Buffers:

  • Short-lived buffers
  • Small to medium sized buffers
  • When allocation speed is critical
  • When working with arrays anyway

Direct Buffers:

  • Long-lived buffers
  • Large buffers (>64KB)
  • Frequent I/O operations
  • Zero-copy requirements
  • Native memory access needed

Wrapped Buffers:

  • Existing array data
  • Interoperability with array-based APIs
  • Avoiding data copying

Common Buffer Patterns #

Read-Process-Write Cycle #

// Allocate buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);

// Read from channel
while (channel.read(buffer) != -1) {
    buffer.flip();      // Prepare for reading
    
    // Process data
    while (buffer.hasRemaining()) {
        byte b = buffer.get();
        // Process byte
    }
    
    buffer.clear();    // Prepare for next read
}

Scattering Reads #

// Create buffer array for scattering read
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(10240);
ByteBuffer[] buffers = {header, body};

// Read into multiple buffers
channel.read(buffers);

// Process each buffer
for (ByteBuffer buf : buffers) {
    buf.flip();
    // Process buffer contents
}

Gathering Writes #

// Create buffer array for gathering write
ByteBuffer header = ByteBuffer.wrap("HEADER".getBytes());
ByteBuffer body = ByteBuffer.wrap("BODY".getBytes());
ByteBuffer[] buffers = {header, body};

// Write from multiple buffers
channel.write(buffers);

Buffer Slicing #

// Create a slice of existing buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Fill buffer with data...
buffer.position(256).limit(768);
ByteBuffer slice = buffer.slice();

// Slice shares data with original buffer
slice.put(0, (byte)42);  // Modifies original buffer too

Duplicate Buffers #

// Create independent buffer with same content
ByteBuffer original = ByteBuffer.allocate(1024);
// Fill buffer with data...
ByteBuffer duplicate = original.duplicate();

// Duplicate has independent position/limit/mark
duplicate.position(512);
// Doesn't affect original's position

Best Practices #

  1. Reuse Buffers: Allocate buffers once and reuse them to reduce GC pressure
  2. Size Appropriately: Choose buffer sizes based on expected data volumes
  3. Use Direct Buffers: For large, long-lived buffers involved in frequent I/O
  4. Flip Properly: Always call flip() before reading from a buffer you’ve written to
  5. Clear Properly: Always call clear() or compact() before reusing a buffer
  6. Check Capacity: Verify buffer capacity before operations to avoid exceptions
  7. Use Bulk Operations: Prefer bulk get()/put() operations over element-by-element access
  8. Monitor Memory: Direct buffers don’t show up in heap usage - monitor native memory

Common Pitfalls #

  1. Forgetting to Flip: Reading from a buffer without calling flip() first
  2. Buffer Overflow: Writing beyond capacity without checking remaining()
  3. Buffer Underflow: Reading beyond limit without checking hasRemaining()
  4. Memory Leaks: Not releasing direct buffers properly
  5. Thread Safety: Buffers are not thread-safe for concurrent access
  6. Endianness: Forgetting to set byte order for multi-byte data
  7. Mark Invalid: Using reset() without first calling mark()

Buffer Internals #

HeapByteBuffer Implementation #

class HeapByteBuffer extends ByteBuffer {
    final byte[] hb;       // The underlying byte array
    final int offset;      // The offset into the array
    
    HeapByteBuffer(int cap, int lim) {
        super(-1, 0, lim, cap, null);
        hb = new byte[cap];
        offset = 0;
    }
    
    HeapByteBuffer(byte[] buf, int off, int len) {
        super(-1, off, off + len, buf.length, null);
        hb = buf;
        offset = 0;
    }
    
    public byte get() {
        return hb[ix(nextGetIndex())];
    }
    
    public byte get(int i) {
        return hb[ix(checkIndex(i))];
    }
    
    public ByteBuffer put(byte x) {
        hb[ix(nextPutIndex())] = x;
        return this;
    }
    
    public ByteBuffer put(int i, byte x) {
        hb[ix(checkIndex(i))] = x;
        return this;
    }
    
    int ix(int i) {
        return i + offset;
    }
}

DirectByteBuffer Implementation #

class DirectByteBuffer extends ByteBuffer {
    private final long address;  // Native memory address
    private final Cleaner cleaner; // For freeing native memory
    
    DirectByteBuffer(int cap) {
        super(-1, 0, cap, cap, null);
        address = UNSAFE.allocateMemory(cap);
        cleaner = Cleaner.create(this, new Deallocator(address));
    }
    
    public byte get() {
        return UNSAFE.getByte(address + ix(nextGetIndex()));
    }
    
    public byte get(int i) {
        return UNSAFE.getByte(address + ix(checkIndex(i)));
    }
    
    public ByteBuffer put(byte x) {
        UNSAFE.putByte(address + ix(nextPutIndex()), x);
        return this;
    }
    
    public ByteBuffer put(int i, byte x) {
        UNSAFE.putByte(address + ix(checkIndex(i)), x);
        return this;
    }
    
    private static class Deallocator implements Runnable {
        private static final long address;
        Deallocator(long address) { this.address = address; }
        public void run() {
            UNSAFE.freeMemory(address);
        }
    }
}

Performance Optimization Techniques #

Buffer Pooling #

// Simple buffer pool
public class BufferPool {
    private final BlockingQueue<ByteBuffer> pool;
    
    public BufferPool(int size, int bufferSize) {
        pool = new ArrayBlockingQueue<>(size);
        for (int i = 0; i < size; i++) {
            pool.add(ByteBuffer.allocateDirect(bufferSize));
        }
    }
    
    public ByteBuffer acquire() throws InterruptedException {
        ByteBuffer buffer = pool.take();
        buffer.clear();
        return buffer;
    }
    
    public void release(ByteBuffer buffer) {
        buffer.clear();
        pool.put(buffer);
    }
}

Zero-Copy Techniques #

// Memory-mapped file for zero-copy I/O
FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
MappedByteBuffer buffer = channel.map(
    FileChannel.MapMode.READ_ONLY, 0, channel.size()
);

// Direct buffer to socket (zero-copy)
SocketChannel socketChannel = SocketChannel.open();
socketChannel.write(buffer);

Composite Buffers #

// Combine multiple buffers for single I/O operation
ByteBuffer[] buffers = new ByteBuffer[] {
    ByteBuffer.wrap("HEADER".getBytes()),
    dataBuffer,
    ByteBuffer.wrap("FOOTER".getBytes())
};

// Single write operation for all buffers
channel.write(buffers);