🚀AdvancedAdvanced IO

NIO Buffers and Channels

Use java.nio ByteBuffer, FileChannel, and Selector for high-performance non-blocking I/O.

Overview

Java NIO (New I/O, java.nio) provides an alternative to the classic stream-based I/O that enables non-blocking and memory-mapped file operations. Three core abstractions drive NIO: - Buffer: a fixed-capacity container for data (ByteBuffer, IntBuffer, etc.) - Channel: a bidirectional connection to an I/O resource (FileChannel, SocketChannel) - Selector: a multiplexer that monitors multiple channels for events, enabling a single thread to handle many connections NIO shines for high-throughput file operations (memory-mapped files, direct buffers, scatter-gather I/O) and scalable network servers (non-blocking sockets with Selector). Classic I/O streams remain simpler for routine file operations.

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.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 4

What does ByteBuffer.flip() do?