Course topics

By WebNest Studio Engineering Team

Advanced Java Tutorial

Java NIO Basics: Channels and Buffers

Classic java.io (the "old I/O" package built around Streams like FileInputStream and FileOutputStream) reads and writes data one byte or char at a time, in a blocking, sequential fashion. Java NIO ("New I/O", introduced in Java 1.4 and significantly expanded since) offers an alternative model built around Buffers and Channels that supports block-based transfer, direct memory access, and non-blocking I/O — capabilities the original streams-based API was never designed for.

NIO is not a strict replacement for java.io — both are still widely used, and for many simple file-reading tasks, java.io (or the higher-level java.nio.file.Files utility methods) remains simpler to write. But understanding NIO's Buffer/Channel model is essential for anything involving high-throughput I/O, non-blocking network servers, or memory-mapped file access.

Why it matters

High-performance servers, including the ones underneath Netty, Tomcat's NIO connector and Kafka, read and write through channels and buffers. The buffer's position and limit, and the flip() call that switches it from writing to reading, are the part everyone gets wrong at first; a forgotten flip reads nothing at all.

Buffer: A Fixed-Size Container With Position and Limit

A Buffer (e.g. ByteBuffer, CharBuffer) is a fixed-capacity container for a primitive data type that data is read into or written out of in bulk. Unlike a Stream, which just pushes bytes through one at a time, a Buffer is a block of memory you actively manage using three key properties:

  • capacity — the fixed, unchanging total size of the buffer, set when it is created.
  • position — the index of the next element to be read or written; it advances as you get/put data.
  • limit — the index of the first element that should not be read or written; it marks the boundary of valid data currently in the buffer.

The flip() and clear() Idiom

A very common NIO pattern is writing data into a buffer (e.g., reading from a channel into it), then calling buffer.flip() before reading that data back out. flip() sets limit to the current position (marking exactly how much data is valid) and resets position to 0, effectively switching the buffer from "fill mode" to "drain mode." After consuming the data, buffer.clear() resets position to 0 and limit to capacity, preparing the buffer to be filled again. Forgetting to call flip() before reading is one of the most common NIO bugs, since without it, you'd try to read starting from wherever the write left off, getting no data or garbage.

Channel: The Conduit Between Buffers and I/O Sources

A Channel represents an open connection to an I/O source or destination — a file (FileChannel), a network socket (SocketChannel), or other resources. Unlike a Stream, a Channel is bidirectional-capable in principle (a FileChannel opened appropriately can both read and write) and always transfers data into or out of Buffers, never as loose individual bytes. This block-oriented design, combined with the JVM's ability to allocate "direct" buffers backed by native memory outside the regular garbage-collected heap, allows the OS to move data more efficiently — sometimes bypassing an extra copy step that classic streams require, and enabling non-blocking mode for network channels so a single thread can manage many connections instead of blocking one thread per connection.

Reading a File with FileChannel

A typical FileChannel read loop allocates a ByteBuffer, repeatedly calls channel.read(buffer) to fill it, flips the buffer to switch to reading mode, processes or extracts the data, then clears the buffer before the next read. The channel's read() method returns the number of bytes read, or -1 when the end of the file/stream has been reached, which is the loop's termination condition — directly analogous to how InputStream.read() signals end-of-stream, but operating on whole blocks of bytes at once via the buffer instead of one byte/char at a time.

Examples

Reading a text file using FileChannel and ByteBuffer

Java
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;

public class FileChannelDemo {
    public static void main(String[] args) throws IOException {
        String path = "nio-demo.txt";

        // Write some sample content first so the read has something to find.
        try (FileOutputStream fos = new FileOutputStream(path)) {
            fos.write("Hello from Java NIO!".getBytes(StandardCharsets.UTF_8));
        }

        // Read the file back using a FileChannel and a ByteBuffer.
        try (FileChannel channel = FileChannel.open(Paths.get(path), StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(64);
            StringBuilder content = new StringBuilder();

            int bytesRead = channel.read(buffer);
            while (bytesRead != -1) {
                buffer.flip(); // switch from "fill" mode to "drain" mode
                while (buffer.hasRemaining()) {
                    content.append((char) buffer.get());
                }
                buffer.clear(); // reset for the next read
                bytesRead = channel.read(buffer);
            }

            System.out.println("File contents: " + content);
        }
    }
}
Output
File contents: Hello from Java NIO!

The flip()/clear() lifecycle on a ByteBuffer, shown step by step

Java
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

public class BufferLifecycleDemo {
    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(20);
        System.out.println("After allocate -> capacity=" + buffer.capacity()
                + " position=" + buffer.position() + " limit=" + buffer.limit());

        buffer.put("Hi NIO".getBytes(StandardCharsets.UTF_8));
        System.out.println("After put -> position=" + buffer.position() + " limit=" + buffer.limit());

        buffer.flip();
        System.out.println("After flip -> position=" + buffer.position() + " limit=" + buffer.limit());

        byte[] data = new byte[buffer.remaining()];
        buffer.get(data);
        System.out.println("Read back: " + new String(data, StandardCharsets.UTF_8));

        buffer.clear();
        System.out.println("After clear -> position=" + buffer.position() + " limit=" + buffer.limit());
    }
}
Output
After allocate -> capacity=20 position=0 limit=20
After put -> position=6 limit=20
After flip -> position=0 limit=6
Read back: Hi NIO
After clear -> position=0 limit=20

Common Mistakes

  • Forgetting to call flip() after writing data into a buffer and before reading it back out — this reads from the wrong position and typically yields empty or garbage data.
  • Confusing capacity with limit — capacity never changes for a given buffer instance, while limit changes constantly as you flip/clear/compact the buffer.
  • Assuming NIO Channels always mean "non-blocking" — a FileChannel used the way shown above is still a blocking call; non-blocking mode is a separate, explicit configuration mostly relevant to socket channels and Selectors.
  • Choosing raw NIO Channels/Buffers for a simple one-off file read where java.nio.file.Files.readAllLines() or java.io.BufferedReader would be far simpler and just as appropriate.

Exercise

Put the bytes for h and i into an 8-byte buffer and print its position and limit. Flip the buffer and print the position and limit again. Then read the remaining bytes and print them as text.

Expected output: 2 8, 0 2, hi (one per line)

Java · starter code
import java.nio.ByteBuffer;

public class BufferStates {
    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(8);

        // TODO: put the bytes 'h' and 'i', then print position and limit
        // TODO: flip, then print position and limit
        // TODO: read the remaining bytes into a StringBuilder and print it
    }
}
Show hints
  • A character is stored with buffer.put((byte) 'h').
  • Read while buffer.hasRemaining(), appending (char) buffer.get().
Show solution
Java · solution
import java.nio.ByteBuffer;

public class BufferStates {
    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(8);

        buffer.put((byte) 'h');
        buffer.put((byte) 'i');
        System.out.println(buffer.position() + " " + buffer.limit()); // 2 8

        buffer.flip();
        System.out.println(buffer.position() + " " + buffer.limit()); // 0 2

        StringBuilder text = new StringBuilder();
        while (buffer.hasRemaining()) {
            text.append((char) buffer.get());
        }
        System.out.println(text); // hi
    }
}

Quick quiz

1. What does flip() do to a buffer?

  • A. Erases its contents
  • B. Reverses the bytes
  • C. Sets the limit to the current position and the position to zero, ready for reading
  • D. Doubles its capacity
Show answer

C. It switches the buffer from being written into to being read from.

2. What does clear() do?

  • A. Resets position to zero and limit to capacity, without erasing the bytes
  • B. Deletes the buffer
  • C. Fills the buffer with zeros
  • D. Closes the channel
Show answer

A. The old bytes are simply overwritten by the next writes.

3. How does a channel differ from a stream?

  • A. A channel cannot read files
  • B. A channel reads and writes through buffers and can work in both directions and in non-blocking mode
  • C. A stream is faster
  • D. A channel works only with text
Show answer

B. A stream moves bytes in one direction, one call at a time, and always blocks.

Interview questions

Explain position, limit and capacity of a buffer.

Show answer

Capacity is the fixed size of the buffer. Position is the index of the next element to read or write. Limit is the first index that must not be read or written. While filling a buffer, the limit equals the capacity and the position advances. flip() then sets the limit to the position and the position to zero, so that exactly the bytes written can be read back.

What is the difference between java.io and java.nio?

Show answer

java.io is stream-oriented and blocking: each read or write moves bytes one way and the thread waits until it completes, so a server needs a thread per connection. java.nio is buffer-oriented: data is read into buffers through channels, which can be non-blocking, and a Selector lets one thread watch many channels and act only on those that are ready. That scales to many more connections.

Key Points to Remember

  • NIO is built around Buffers (fixed-capacity containers with position/limit/capacity) and Channels (conduits to files, sockets, etc.), unlike classic streams which move one byte/char at a time.
  • flip() switches a buffer from write mode to read mode (limit = position, position = 0); clear() resets it for writing again (position = 0, limit = capacity).
  • FileChannel.read(buffer) returns the number of bytes read, or -1 at end of file, mirroring how InputStream signals end-of-stream but operating in blocks.
  • NIO enables more efficient bulk transfers and, for network channels, non-blocking I/O so one thread can manage many connections — a capability classic blocking streams do not offer.

Practice in the playground

Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.

Run the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

These examples need a JDK project with their libraries on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.

WebNest Studio

Written by

WebNest Studio Engineering Team

Practical lessons from developers working with Java, Python, React, databases and production systems.

Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering