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.
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.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.