NIO Buffers and Channels
Use java.nio ByteBuffer, FileChannel, and Selector for high-performance non-blocking I/O.
Overview
Key Points
- •ByteBuffer.allocate(n) creates a heap buffer; allocateDirect(n) creates an off-heap direct buffer
- •Buffer states: capacity (max), limit (active data end), position (current read/write cursor)
- •flip() sets limit = position and position = 0 (switches from write to read mode)
- •rewind() sets position = 0 without changing limit (re-read)
- •clear() resets for writing (position = 0, limit = capacity)
- •FileChannel.open(path, options) opens a file channel
- •channel.read(buffer) and channel.write(buffer) transfer data
- •Files.newByteChannel() is the modern way to open a channel via the Path API
- •Memory-mapped files via channel.map() allow direct memory access to file contents
- •Selector enables event-driven non-blocking I/O for network servers
Examples
Reading and writing files with FileChannel and ByteBuffer
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
public class Main {
public static void main(String[] args) throws Exception {
Path path = Path.of("test-nio.txt");
// Write using FileChannel
try (FileChannel wc = FileChannel.open(path,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING)) {
ByteBuffer buf = ByteBuffer.wrap("Hello NIO World".getBytes());
wc.write(buf);
}
// Read using FileChannel
try (FileChannel rc = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(64);
int bytesRead = rc.read(buf);
buf.flip(); // switch to read mode
byte[] bytes = new byte[buf.limit()];
buf.get(bytes);
System.out.println("Read " + bytesRead + " bytes: " + new String(bytes));
}
Files.deleteIfExists(path);
}
}flip() is critical: after write operations fill the buffer, flip() sets position=0 and limit=bytesWritten so subsequent reads consume exactly the written data. Forgetting flip() reads from the wrong position.
Memory-mapped file for fast random access
import java.nio.*;
import java.nio.channels.*;
import java.nio.file.*;
public class Main {
public static void main(String[] args) throws Exception {
Path path = Path.of("mmap-test.bin");
// Create a 1MB file
try (FileChannel fc = FileChannel.open(path,
StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE)) {
MappedByteBuffer mmap = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024 * 1024);
// Write directly to memory (no system call per write)
for (int i = 0; i < 1000; i++) {
mmap.putInt(i * 4, i * 100);
}
// Read back
mmap.force(); // flush to disk
System.out.println("Value at index 42: " + mmap.getInt(42 * 4)); // 4200
System.out.println("Value at index 0: " + mmap.getInt(0)); // 0
}
Files.deleteIfExists(path);
}
}Memory-mapped I/O maps file contents directly into virtual memory. Reads and writes go through the OS page cache without explicit read/write system calls per operation. This is the fastest way to do random access on large files.
ByteBuffer state machine: flip, rewind, clear
import java.nio.ByteBuffer;
public class Main {
public static void main(String[] args) {
ByteBuffer buf = ByteBuffer.allocate(8);
System.out.println("Initial: cap=" + buf.capacity() + " pos=" + buf.position() + " lim=" + buf.limit());
buf.put((byte) 1);
buf.put((byte) 2);
buf.put((byte) 3);
System.out.println("After 3 puts: pos=" + buf.position() + " lim=" + buf.limit());
buf.flip(); // ready to read
System.out.println("After flip: pos=" + buf.position() + " lim=" + buf.limit());
System.out.println("Read: " + buf.get() + " " + buf.get()); // 1 2
System.out.println("After 2 gets: pos=" + buf.position() + " lim=" + buf.limit());
buf.rewind(); // re-read from start
System.out.println("After rewind: pos=" + buf.position() + " lim=" + buf.limit());
buf.clear(); // reset for writing
System.out.println("After clear: pos=" + buf.position() + " lim=" + buf.limit());
}
}Understanding these state transitions is essential for correct NIO usage. The most common bug is writing to a buffer and then calling get() without flip() first, which reads from the wrong position.
Common Mistakes
- Forgetting to call flip() before reading from a buffer that was just written to. The position will be at the end of written data and get() will read garbage or throw BufferUnderflowException.
- Using a buffer after clear() as if it still contains data. clear() resets position and limit; it does NOT erase data, but subsequent writes will overwrite it.
- Using allocateDirect() for small or short-lived buffers. Direct buffers bypass the Java heap and live in native memory. Allocation is expensive. Use allocate() for small buffers and direct() for large, long-lived ones used with I/O channels.
- Not calling channel.close() or using try-with-resources. Unclosed channels leak file descriptors.
Note
For simple file reading/writing, stick with Files.readAllBytes, Files.writeString, or BufferedReader/Writer. Use FileChannel and ByteBuffer only when you need maximum performance, memory mapping, scatter-gather I/O, or non-blocking network I/O.
Exercises
Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.
Completed: 0 / 2 exercises
Knowledge Check
Question 1 of 4