Backend Development 12 min read

Understanding Java NIO Buffers: Creation, Core Properties, and Operations

This article explains Java NIO buffers, covering how to create them with allocate() and allocateDirect(), describing their four core properties (position, limit, capacity, mark), and detailing essential methods such as put(), get(), flip(), rewind(), clear(), mark(), reset(), hasRemaining() and remaining() with code examples.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Understanding Java NIO Buffers: Creation, Core Properties, and Operations

Buffers are containers for primitive data types defined in the java.nio package; all buffers extend the abstract Buffer class and are used to interact with NIO channels, where data is written to a buffer from a channel and read back into a channel.

1. Creating Buffers

The essence of a buffer is an array, and for each primitive type (except boolean ) there is a corresponding buffer class such as ByteBuffer , IntBuffer , etc. Buffers are created by calling the allocate() method with a capacity:

// Create a 1 KB buffer
ByteBuffer buf = ByteBuffer.allocate(1024);

For high‑performance scenarios a direct buffer can be created with allocateDirect() . The allocate() method creates a non‑direct buffer, while allocateDirect() creates a direct buffer.

2. Four Core Properties of a Buffer

All buffers share four integer fields defined in the abstract Buffer class: mark , position , limit , and capacity . These act like abstract pointers that control read/write operations.

private int mark = -1;
private int position = 0;
private int limit;
private int capacity;

The relationship among them is 0 <= mark <= position <= limit <= capacity . Their meanings are:

position : the index of the next element to be read or written.

mark : a saved position set by mark() , which can be restored with reset() .

limit : the index of the first element that should not be read or written.

capacity : the fixed size of the underlying array.

2.1 Initial Pointer State

When a buffer with capacity 5 is allocated, its initial state is position=0, limit=5, capacity=5, mark=-1 .

2.2 State After Writing Data

In write mode, limit and position differ; after putting data the position advances while the limit remains unchanged.

// Write mode
byteBuffer.put("Tom".getBytes());

2.3 State After Reading Data

In read mode, the buffer is flipped first, then get() reads bytes, moving the position forward.

// Read mode
byteBuffer.get();

3. Core Buffer Methods

3.1 Data Access

put() stores data into the buffer; get() retrieves data. A flip() must be called between writing and reading to switch modes.

3.2 flip(), rewind(), clear()

flip() : switches from write to read mode by setting limit = position and position = 0 .

rewind() : resets position to zero without changing limit , allowing rereading.

clear() : resets position to zero and limit to capacity , discarding the mark; the data remains but is considered forgotten.

3.3 mark() and reset()

mark() records the current position; reset() restores the position to the previously set mark.

3.4 hasRemaining() and remaining()

hasRemaining() returns true if position < limit .

remaining() returns limit - position , the number of elements left to read.

3.5 Example Program

The following program demonstrates buffer creation, writing, flipping, reading, rewinding, clearing, marking, resetting, and querying remaining elements, printing the values of position , limit , and capacity after each operation.

// Allocate 1 KB buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
System.out.println("=============allocate()===========");
System.out.println("position = " + byteBuffer.position());
System.out.println("limit = " + byteBuffer.limit());
System.out.println("capacity = " + byteBuffer.capacity());

// put data
String name = "abcde";
byteBuffer.put(name.getBytes());
System.out.println("=============put()===========");
System.out.println("position = " + byteBuffer.position());
System.out.println("limit = " + byteBuffer.limit());
System.out.println("capacity = " + byteBuffer.capacity());

// flip
byteBuffer.flip();
System.out.println("=============flip()===========");
System.out.println("position = " + byteBuffer.position());
System.out.println("limit = " + byteBuffer.limit());
System.out.println("capacity = " + byteBuffer.capacity());

// get data
byte[] dst = new byte[byteBuffer.limit()];
byteBuffer.get(dst);
System.out.println("=============get()===========");
System.out.println("position = " + byteBuffer.position());
System.out.println("limit = " + byteBuffer.limit());
System.out.println("capacity = " + byteBuffer.capacity());
System.out.println(new String(dst));

// rewind
byteBuffer.rewind();
System.out.println("=============rewind()===========");
System.out.println("position = " + byteBuffer.position());
System.out.println("limit = " + byteBuffer.limit());
System.out.println("capacity = " + byteBuffer.capacity());

// clear
byteBuffer.clear();
System.out.println("=============clear()===========");
System.out.println("position = " + byteBuffer.position());
System.out.println("limit = " + byteBuffer.limit());
System.out.println("capacity = " + byteBuffer.capacity());

// mark, reset example
ByteBuffer buf = ByteBuffer.allocate(5);
buf.put("abcde".getBytes());
buf.flip();
byte[] dst2 = new byte[buf.limit()];
buf.get(dst2, 0, 2);
System.out.println("First read: " + new String(dst2));
System.out.println("position: " + buf.position());
buf.mark();
buf.get(dst2, 2, 2);
System.out.println("Second read: " + new String(dst2));
System.out.println("position: " + buf.position());
buf.reset();
System.out.println("reset to mark");
System.out.println("position: " + buf.position());

// remaining
if (buf.hasRemaining()) {
    System.out.println(buf.remaining());
    System.out.println("limit - position = " + (buf.limit() - buf.position()));
}

4. Direct vs. Non‑Direct Buffers

Byte buffers can be either direct or non‑direct. Non‑direct buffers are allocated on the JVM heap using allocate() and involve copying data between kernel and user space. Direct buffers, created with allocateDirect() , reside outside the heap in physical memory, avoiding the extra copy and offering better performance, especially when used with FileChannel.map() to create a MappedByteBuffer .

Note that direct buffers are not managed by the garbage collector; their allocation and deallocation are costly, so they should be used only when performance requirements justify the overhead.

BackendJavaPerformanceNIOBufferByteBuffer
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.