Fundamentals 19 min read

Understanding Java NIO: Core Components and How They Differ from Traditional IO

This article explains Java NIO introduced in Java 1.4, covering its core components—Channels, Buffers, and Selectors—detailing channel types, buffer operations, non‑blocking behavior, selector usage, and code examples such as FileChannel and Pipe, and compares NIO with classic IO.

ZhiKe AI
ZhiKe AI
ZhiKe AI
Understanding Java NIO: Core Components and How They Differ from Traditional IO

Introduction

Java NIO was introduced in Java 1.4. Its core concepts are Channels, Buffers, and Selectors.

Core components

Channels

Buffers

Selectors

Channel and Buffer

All I/O starts with a Channel. Data is read from a Channel into a Buffer or written from a Buffer to a Channel.

Channel implementations

FileChannel – reads/writes files

DatagramChannel – UDP network I/O

SocketChannel – TCP network I/O

ServerSocketChannel – listens for incoming TCP connections and creates a SocketChannel for each

Buffer types

ByteBuffer

CharBuffer

DoubleBuffer

FloatBuffer

IntBuffer

LongBuffer

ShortBuffer

Selector

A Selector allows a single thread to monitor multiple Channels. Register Channels with a Selector and call select(), which blocks until at least one registered Channel is ready.

Java NIO vs. IO

IO is stream‑oriented and blocking; NIO is buffer‑oriented and can be non‑blocking. Selectors exist only in NIO.

Stream vs. Buffer – Classic IO processes bytes directly from a stream without intermediate storage and cannot move backwards. NIO reads data into a Buffer that can be repositioned, giving more flexible processing at the cost of explicit buffer management.

Blocking vs. Non‑blocking – In classic IO a thread calling read() or write() blocks until the operation completes. In NIO a non‑blocking call returns immediately if no data is available, allowing the thread to handle other Channels.

Use NIO when managing thousands of low‑traffic connections; classic IO may be preferable for a few high‑bandwidth connections.

Channel details

Channels are bidirectional, can be asynchronous, and always interact with a Buffer.

FileChannel example

public class FileChannelTest {
    public static void main(String[] args) {
        try {
            RandomAccessFile aFile = new RandomAccessFile("1.txt", "rw");
            FileChannel channel = aFile.getChannel();
            ByteBuffer buf = ByteBuffer.allocate(48);
            int bytesRead = channel.read(buf);
            while (bytesRead != -1) {
                buf.flip();
                while (buf.hasRemaining()) {
                    System.out.print((char) buf.get());
                }
                buf.clear();
                bytesRead = channel.read(buf);
            }
            aFile.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

The call to buf.flip() switches the Buffer from write mode to read mode.

Buffer operations

Typical workflow: write → flip() → read → clear() or compact().

Write data into the Buffer

Call flip() to switch to read mode

Read data from the Buffer

Call clear() or compact() to prepare for the next write

Key properties:

capacity – total size of the Buffer

position – index for the next read or write

limit – in write mode, the maximum amount that can be written (equals capacity); in read mode, the amount of data available to read (set to the previous position)

Allocation example: ByteBuffer buf = ByteBuffer.allocate(48); Writing to a Buffer can be done via a Channel read ( int bytesRead = inChannel.read(buf);) or buf.put(...). flip() must be called once before reading; calling it multiple times can cause errors.

Reading can be performed by a Channel write ( int bytesWritten = outChannel.write(buf);) or byte b = buf.get();. rewind() resets position to 0 while keeping limit unchanged. clear() resets position to 0 and sets limit to capacity, discarding all data. compact() moves any unread data to the beginning of the Buffer and prepares the remaining space for writing without losing the unread bytes. mark() and reset() let you remember a specific position and later return to it.

Scatter/Gather

Scatter reads distribute data from a Channel into multiple Buffers; gather writes combine data from multiple Buffers into a single Channel.

Channel‑to‑Channel data transfer

If either source or target is a FileChannel, data can be transferred directly:

toChannel.transferFrom(position, count, fromChannel);
position

is the start offset in the target file; count is the maximum number of bytes to transfer. For SocketChannel, only currently available data may be transferred.

Selector usage

Create a Selector: Selector selector = Selector.open(); Configure a Channel to non‑blocking and register it:

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

The second argument is the interest set, indicating which events (CONNECT, ACCEPT, READ, WRITE) you want to be notified about.

Selection methods: select() – blocks until at least one registered Channel is ready select(long timeout) – blocks up to the given timeout selectNow() – non‑blocking, returns immediately

After select() returns, retrieve the ready keys and iterate:

Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()) {
        // handle accept
    } else if (key.isConnectable()) {
        // handle connect
    } else if (key.isReadable()) {
        // handle read
    } else if (key.isWritable()) {
        // handle write
    }
    keyIterator.remove();
}
Selector.wakeup()

forces a blocked select() to return immediately. selector.close() invalidates all registered SelectionKeys but does not close the Channels.

Pipe

A Pipe provides a one‑way data connection between two threads, consisting of a source Channel and a sink Channel.

Create a Pipe: Pipe pipe = Pipe.open(); Write to the sink:

Pipe.SinkChannel sinkChannel = pipe.sink();
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while (buf.hasRemaining()) {
    sinkChannel.write(buf);
}

Read from the source:

Pipe.SourceChannel sourceChannel = pipe.source();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buf);
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ChannelsJava NIONon-blocking IOSelectorsBuffersFileChannelScatter/Gather
ZhiKe AI
Written by

ZhiKe AI

We dissect AI-era technologies, tools, and trends with a hardcore perspective. Focused on large models, agents, MCP, function calling, and hands‑on AI development. No fluff, no hype—only actionable insights, source code, and practical ideas. Get a daily dose of intelligence to simplify tech and make efficiency tangible.

0 followers
Reader feedback

How this landed with the community

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.