📁 I/O, Files & Networking · Intermediate

NIO channels & buffers in Java

ByteBuffer, channels, non-blocking I/O and selectors at a high level.

🧩 The mysteryYou put the byte 7 into a buffer and immediately read it back. Java hands you… 0. The 7 is right there! Why can't Java see it?

Channels and buffers

In NIO, a channel is a connection to a file or socket, and data never moves directly to or from it — it always goes through a buffer. FileChannel even supports memory mapping a file.

ByteBuffer buf = ByteBuffer.allocate(1024);
int n = channel.read(buf); // fill buf
buf.flip();                // now read

Three markers

A buffer tracks capacity (fixed when allocated, never grows), position (the next index to read or write) and limit (where valid data ends). After allocate(8): position 0, limit 8, capacity 8.

flip(): from writing to reading

**flip() sets limit = position and position = 0 — "the data I just wrote is now ready to read". clear() or compact()** prepare it for writing again.

var buf = ByteBuffer.allocate(8);
buf.put((byte) 1).put((byte) 2);
// position 2, limit 8
buf.flip();
// position 0, limit 2
🔮 Predict it

After the flip

What does this print?

var buf = ByteBuffer.allocate(10);
buf.put(new byte[]{5, 6, 7});
buf.flip();
System.out.println(buf.remaining());
System.out.println(buf.get());
  1. 7 0
  2. 3 5
  3. 10 5
Show the answer

3 — after flip, position 0 and limit 3, so 3 bytes remain. get() reads index 0: **5**.

⚠️ The trap

Forgetting flip()

Without flip(), position still points after your data. get() reads the next untouched slot — a 0, not your 7. This is the classic NIO bug.

var buf = ByteBuffer.allocate(4);
buf.put((byte) 7);  // position 1
buf.remaining();    // 3 unused slots
buf.get();          // 0: reads index 1!

Selectors: one thread, many channels

Socket channels can be non-blocking. Register many of them with a **Selector**, and select() reports which ones are ready to read or write — so one thread can serve thousands of connections.

💼 In the real world

Who uses NIO

High-performance servers and frameworks — Netty, Tomcat's NIO connector, Kafka — are built on channels and selectors. Most app developers use them indirectly, but buffer flip bugs and memory-mapped files show up in performance-critical code.

Key takeaways

  1. Buffer: capacity, position, limit
  2. flip(): limit = position, position = 0 — ready to read
  3. clear()/compact() prepare the buffer for writing again
  4. Selector: one thread, many non-blocking channels
🤯 Did you know?

NIO stands for "New I/O" — introduced in Java 1.4 back in 2002, so the "new" API is now over twenty years old.

Practice questions

What does this print?

ByteBuffer buf = ByteBuffer.allocate(8);
buf.put((byte) 1).put((byte) 2);
System.out.println(
    buf.position() + " " + buf.limit());
buf.flip();
System.out.println(
    buf.position() + " " + buf.limit());
  1. 2 8 0 2
  2. 2 8 0 8
  3. 0 8 2 8
  4. 2 2 0 2
Check your answer

2 8 0 2. After two puts, position is 2 and the limit is still the capacity, 8. flip() sets limit to 2 (the end of the data) and position back to 0.

What does this print?

ByteBuffer buf = ByteBuffer.allocate(4);
buf.put((byte) 7);
System.out.println(buf.remaining());
System.out.println(buf.get());
  1. 1 7
  2. 3 0
  3. 3 7
  4. Throws BufferUnderflowException
Check your answer

3 0. Without flip(), position is 1, so remaining() counts the 3 unused slots and get() reads index 1 — an untouched 0, not the 7. Forgetting flip() is the classic NIO bug.

Next: JSON — the format every API speaks, which the JDK still can't parse on its own.