☕ Java

NIO Overview

Java NIO (New I/O), introduced in Java 1.4 as the java.nio package, is a collection of APIs that complement and in many cases supersede the original java.io stream model. NIO introduces three foundational abstractions absent from java.io: buffers (typed, fixed-capacity containers for data that support direct memory allocation outside the Java heap), channels (bidirectional, closeable connections to I/O entities that read and write buffers rather than individual bytes), and selectors (multiplexers that let a single thread monitor multiple channels for readiness, enabling non-blocking I/O at scale). Java 7 extended NIO with the java.nio.file package, known as NIO.2 or JSR-203, which provides the Path interface, the Files utility class, the FileSystem abstraction, WatchService for file system event monitoring, and the AsynchronousFileChannel API. Together, NIO and NIO.2 enable four I/O models unavailable in java.io: non-blocking I/O (channel reads and writes return immediately if data is unavailable), asynchronous I/O (I/O operations are submitted and a callback or Future signals completion), memory-mapped I/O (a file is mapped directly into the JVM's virtual address space for zero-copy access), and scatter/gather I/O (reading into or writing from multiple buffers in a single system call). This entry covers the conceptual architecture of NIO, the relationship between buffers, channels, and selectors, the four I/O models, the NIO.2 additions and how they relate to the core NIO abstractions, the difference between blocking and non-blocking channels, and which NIO API to use for which problem.

The Three Pillars — Buffers, Channels, and Selectors

Java IO uses streams as its fundamental abstraction: a stream is a sequential, unidirectional flow of bytes, and all reading and writing is byte-at-a-time or array-at-a-time through method calls that block until data is available or written. NIO replaces this model with a different set of primitives that map more directly to how modern operating systems and hardware actually perform I/O. A Buffer is a fixed-capacity, typed container for data. Where java.io streams process data as it flows through method calls, NIO accumulates data in buffers, performs batch operations on those buffers, and then passes the buffers to channels. The core buffer types — ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer — store typed data in a contiguous array (either on the Java heap or in off-heap native memory). A ByteBuffer is the universal type because most I/O ultimately deals in bytes; the other typed buffers are views over a ByteBuffer. Every buffer has three state variables: position (the index of the next element to be read or written), limit (the index of the first element that cannot be read or written), and capacity (the total number of elements the buffer can hold). The flip(), clear(), and compact() operations manipulate these state variables to transition the buffer between write mode and read mode. A Channel is a bidirectional conduit to an I/O entity: a file, a socket, a pipe, or another channel. Unlike streams, channels are bidirectional (a FileChannel can both read and write), and they operate exclusively with buffers rather than individual bytes or arrays. Channels can be blocking (the default, matching stream behavior) or non-blocking (reads and writes return immediately with whatever data is available, or return 0 if nothing is available). The core channel types are FileChannel (random-access file I/O with memory mapping and locking), SocketChannel (TCP client connections), ServerSocketChannel (TCP server acceptors), DatagramChannel (UDP), and Pipe.SinkChannel/Pipe.SourceChannel (inter-thread pipes). A Selector is a multiplexer for non-blocking channels. It allows one thread to monitor many channels simultaneously for readiness: is this channel ready to accept a connection? Is data available to read from this channel? Is there space to write to this channel? The Selector.select() call blocks until at least one registered channel becomes ready, then returns the set of ready channels as SelectionKey objects. This is the foundation of the Reactor pattern: a single thread managing thousands of concurrent network connections, which enables scalable server architectures that don't require one thread per connection. The Selector API maps to OS-level I/O multiplexing: epoll on Linux, kqueue on macOS, IOCP on Windows. NIO.2 (Java 7) adds a fourth abstraction: the FileSystem provider model. Path is a file system path that is not tied to the default file system; custom FileSystem implementations can be plugged in (the JDK's ZIP file system is one example — java.nio.file.FileSystems.newFileSystem(zipPath) opens a ZIP file as a FileSystem with Paths, Files operations, and directory walking). AsynchronousFileChannel and AsynchronousSocketChannel add true asynchronous I/O with CompletionHandler callbacks and Future return values, distinct from non-blocking I/O (which requires Selector-based polling) and from blocking I/O (which blocks until completion).
Java
// ── NIO vs IO — the fundamental difference in code style ─────────────

// java.io style: stream-oriented, blocking, byte-at-a-time
try (InputStream is = new FileInputStream("data.txt");
     BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
    String line;
    while ((line = br.readLine()) != null) {
        process(line);
    }
}

// NIO style: buffer-oriented, channels, explicit position management
try (FileChannel channel = FileChannel.open(Path.of("data.txt"))) {
    ByteBuffer buffer = ByteBuffer.allocate(4096);
    while (channel.read(buffer) != -1) {  // read into buffer
        buffer.flip();                     // switch from write mode to read mode
        while (buffer.hasRemaining()) {
            byte b = buffer.get();         // read from buffer
            process(b);
        }
        buffer.clear();                    // switch back to write mode
    }
}

// ── The four I/O models in NIO ────────────────────────────────────────

// 1. Blocking I/O (default channel behavior — same as java.io):
try (FileChannel fc = FileChannel.open(Path.of("file.txt"))) {
    ByteBuffer buf = ByteBuffer.allocate(1024);
    fc.read(buf);   // blocks until data is available or end of stream
}

// 2. Non-blocking I/O (selector-based, for network channels):
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);   // switch to non-blocking mode
sc.connect(new InetSocketAddress("localhost", 8080));
// read() now returns 0 if no data, -1 at end — never blocks

// 3. Memory-mapped I/O (zero-copy file access):
try (FileChannel fc = FileChannel.open(Path.of("large.bin"))) {
    MappedByteBuffer mapped = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
    // File content accessible directly from virtual memory — no copy to Java heap
    int firstInt = mapped.getInt();
}

// 4. Asynchronous I/O (NIO.2, Java 7+):
AsynchronousFileChannel afc = AsynchronousFileChannel.open(Path.of("async.txt"));
ByteBuffer buf = ByteBuffer.allocate(1024);
afc.read(buf, 0, null, new CompletionHandler<Integer, Void>() {
    public void completed(Integer result, Void attachment) {
        System.out.println("Read " + result + " bytes asynchronously");
    }
    public void failed(Throwable exc, Void attachment) {
        exc.printStackTrace();
    }
});
// Calling thread continues immediately — callback fires when read completes

// ── Selector — one thread, many connections ───────────────────────────
Selector selector = Selector.open();

ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(8080));
server.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    selector.select();   // blocks until at least one channel is ready
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> it = keys.iterator();

    while (it.hasNext()) {
        SelectionKey key = it.next();
        it.remove();   // MUST remove — selector doesn't do this automatically

        if (key.isAcceptable()) {
            SocketChannel client = ((ServerSocketChannel) key.channel()).accept();
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ);
        } else if (key.isReadable()) {
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer readBuf = ByteBuffer.allocate(1024);
            int n = client.read(readBuf);
            if (n == -1) { client.close(); } else { handleRequest(readBuf); }
        }
    }
}

Direct vs Heap Buffers, Scatter/Gather I/O, and the NIO.2 Layer

ByteBuffer can be allocated in two memory regions: heap buffers (ByteBuffer.allocate(n)) live on the Java heap and are subject to garbage collection; direct buffers (ByteBuffer.allocateDirect(n)) live in off-heap native memory, allocated via malloc() or mmap(), and are not garbage collected in the usual sense (they are freed when the ByteBuffer's Cleaner fires, which is separate from Java object finalization). The distinction matters because the OS cannot perform I/O directly to or from heap memory — when a heap ByteBuffer is used with a channel, the JVM internally copies its content to a temporary direct buffer before the system call. Using a direct buffer eliminates this copy, reducing latency and CPU usage for high-throughput I/O paths. However, direct buffer allocation is slower than heap allocation, and direct buffers put pressure on the native memory space rather than the Java heap. The general guidance: use direct buffers for long-lived buffers that participate in many I/O operations; use heap buffers for short-lived buffers or when the copying overhead is not significant. Scatter/gather I/O allows reading from a channel into multiple buffers in a single system call (scatter) or writing from multiple buffers to a channel in a single system call (gather). FileChannel and SocketChannel implement the ScatteringByteChannel and GatheringByteChannel interfaces, providing read(ByteBuffer[] dsts) and write(ByteBuffer[] srcs). The OS-level readv()/writev() system calls handle this in one operation, avoiding both multiple system calls and intermediate buffer copies. This is particularly useful for protocols with fixed-size headers followed by variable-size bodies: create one buffer for the header and one for the body, pass both to a gather write, and the channel sends header+body in a single system call without copying them into a contiguous buffer. NIO.2 sits above the core NIO abstractions but is architecturally separate from the channel/buffer/selector stack. The relationship is: java.nio.file.Path is a pure path abstraction that has nothing to do with open files or channels; java.nio.file.Files is a factory for opening channels (Files.newByteChannel(), Files.newInputStream(), Files.newOutputStream()) and for file system operations (copy, delete, walk, attributes); java.nio.channels.FileChannel is the NIO channel obtained from Files.newByteChannel() or from a FileInputStream/FileOutputStream.getChannel(). NIO.2 does not replace core NIO channel and buffer classes; it provides a better file system path API and factory methods for obtaining channels with explicit OpenOptions. The AsynchronousChannel group (AsynchronousFileChannel, AsynchronousSocketChannel, AsynchronousServerSocketChannel) introduced in NIO.2 uses a thread pool internally to complete I/O and invoke callbacks, rather than the selector model. This is a true asynchronous model (proactor pattern) as opposed to the non-blocking model (reactor pattern) of Selector-based NIO. The OS uses io_uring on Linux, IOCP on Windows, and kqueue on macOS to implement this. For file I/O, asynchronous channels are the only way to achieve truly non-blocking file reads (non-blocking mode on FileChannel is not supported — FileChannel.configureBlocking() throws an exception; file I/O is always blocking at the OS level unless using asynchronous channels or a thread pool that performs blocking I/O on behalf of the caller).
Java
// ── Heap buffer vs direct buffer ─────────────────────────────────────
// Heap buffer: on Java heap, GC-managed, may require copy for I/O:
ByteBuffer heapBuf = ByteBuffer.allocate(4096);

// Direct buffer: off-heap native memory, no copy for I/O, slower to allocate:
ByteBuffer directBuf = ByteBuffer.allocateDirect(4096);

// Performance impact for large file copying:
Path src  = Path.of("large-file.bin");
Path dst  = Path.of("output.bin");

// With heap buffer: JVM allocates temp direct buffer per read/write
long start = System.nanoTime();
try (FileChannel in  = FileChannel.open(src);
     FileChannel out = FileChannel.open(dst,
         StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
    ByteBuffer heap = ByteBuffer.allocate(65536);
    while (in.read(heap) != -1) { heap.flip(); out.write(heap); heap.clear(); }
}
System.out.printf("Heap buffer: %.0f ms%n", (System.nanoTime()-start)/1e6);

// With direct buffer: single DMA path, no intermediate copy:
start = System.nanoTime();
try (FileChannel in  = FileChannel.open(src);
     FileChannel out = FileChannel.open(dst,
         StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
    ByteBuffer direct = ByteBuffer.allocateDirect(65536);
    while (in.read(direct) != -1) { direct.flip(); out.write(direct); direct.clear(); }
}
System.out.printf("Direct buffer: %.0f ms%n", (System.nanoTime()-start)/1e6);

// FileChannel.transferTo — zero-copy at OS level (sendfile() on Linux):
try (FileChannel in  = FileChannel.open(src);
     FileChannel out = FileChannel.open(dst,
         StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
    in.transferTo(0, in.size(), out);  // OS-level zero-copy
}
System.out.printf("transferTo: %.0f ms%n", (System.nanoTime()-start)/1e6); // fastest

// ── Scatter/gather I/O ────────────────────────────────────────────────
// Protocol: 4-byte length header + variable body
ByteBuffer header = ByteBuffer.allocateDirect(4);
ByteBuffer body   = ByteBuffer.allocateDirect(1024);

try (SocketChannel sc = SocketChannel.open(new InetSocketAddress("host", 8080))) {
    // Scatter read: fill header first, then body, in ONE system call:
    ByteBuffer[] buffers = {header, body};
    long totalRead = sc.read(buffers);   // reads into header until full, then body

    header.flip();
    int bodyLength = header.getInt();   // read the 4-byte length prefix

    body.flip();
    byte[] bodyBytes = new byte[bodyLength];
    body.get(bodyBytes);

    // Gather write: write header + body in ONE system call:
    header.clear();
    header.putInt(responseBody.length);
    header.flip();
    ByteBuffer responseBodyBuf = ByteBuffer.wrap(responseBody);
    sc.write(new ByteBuffer[]{header, responseBodyBuf});  // single writev() call
}

// ── NIO.2 as entry point to NIO channels ─────────────────────────────
// Files.newByteChannel — SeekableByteChannel with explicit OpenOptions:
try (SeekableByteChannel sbc = Files.newByteChannel(
        Path.of("data.bin"),
        StandardOpenOption.READ,
        StandardOpenOption.WRITE)) {  // can't do both with FileInputStream
    ByteBuffer buf = ByteBuffer.allocate(512);
    sbc.read(buf);          // read from current position
    sbc.position(0);        // seek to start
    buf.flip();
    sbc.write(buf);         // write modified content back
}

// ── Asynchronous file I/O (NIO.2) ─────────────────────────────────────
try (AsynchronousFileChannel afc = AsynchronousFileChannel.open(
        Path.of("async.bin"), StandardOpenOption.READ)) {

    ByteBuffer buf = ByteBuffer.allocate(4096);
    CompletableFuture<Integer> promise = new CompletableFuture<>();

    afc.read(buf, 0, null, new CompletionHandler<Integer, Void>() {
        public void completed(Integer bytesRead, Void att) {
            buf.flip();
            promise.complete(bytesRead);
        }
        public void failed(Throwable ex, Void att) {
            promise.completeExceptionally(ex);
        }
    });

    // Calling thread is free — not blocked:
    doOtherWork();

    // Retrieve result when needed:
    int n = promise.get(5, TimeUnit.SECONDS);
    System.out.println("Read " + n + " bytes asynchronously");
}

When to Use NIO vs IO vs NIO.2, and the Performance Model

The choice between java.io, NIO, and NIO.2 depends on the use case, not on which API is "newer" or "better." Each has specific strengths. Use java.io (stream-based) for: simple sequential file reading and writing where blocking is acceptable; text file processing with BufferedReader/PrintWriter; object serialization; and any code where simplicity, clarity, and maintainability outweigh throughput. java.io is simpler to read, debug, and reason about than NIO. For most application-level I/O that deals with files, databases, or single connections, java.io is entirely adequate. The one case where java.io becomes truly inadequate is high-connection-count network servers, where thread-per-connection becomes impractical. Use NIO channels and buffers for: high-throughput file copying (transferTo/transferFrom uses sendfile()); memory-mapped file access for random access patterns on large files; large-scale network servers with thousands of concurrent connections (Selector-based non-blocking I/O); scatter/gather I/O for protocols with complex buffer layouts; and direct buffer usage for eliminating unnecessary data copies in I/O-intensive code paths. NIO is more complex than java.io but enables performance that is impossible with streams. Use NIO.2 (java.nio.file) for: all new file system manipulation code — path operations, file creation, deletion, copying, moving, attribute access; directory traversal with Files.walk(); file watching with WatchService; and obtaining FileChannel from Files.newByteChannel() when explicit OpenOptions are needed. NIO.2 does not replace NIO channels for networking; it is specifically about the file system and file I/O. NIO.2 is almost always preferable to java.io.File for file system operations because of correct exception semantics, symbolic link support, atomic operations, and rich metadata. The performance model of NIO can be summarized as: fewer system calls (buffers batch multiple bytes into one system call), fewer memory copies (direct buffers and memory-mapped I/O eliminate Java heap copies), and better concurrency (Selector and asynchronous channels avoid thread-per-connection overhead). Each optimization is targeted: buffer size affects system call count; direct vs heap affects copy count; Selector and async channels affect thread count. Choosing the wrong optimization target (e.g., using direct buffers when the bottleneck is application logic, not I/O copies) adds complexity without benefit.
Java
// ── Decision guide: which API for which problem ──────────────────────

// Case 1: Read a config file, parse, done
// ANSWER: java.io with NIO.2 entry point
String config = Files.readString(Path.of("config.json"), StandardCharsets.UTF_8);

// Case 2: Process a 50GB log file line by line
// ANSWER: java.io stream, NIO.2 path
try (Stream<String> lines = Files.lines(Path.of("app.log"), StandardCharsets.UTF_8)) {
    lines.filter(l -> l.contains("ERROR")).forEach(System.out::println);
}

// Case 3: Copy a 10GB file as fast as possible
// ANSWER: NIO FileChannel.transferTo (zero-copy sendfile)
try (FileChannel in  = FileChannel.open(Path.of("src.bin"));
     FileChannel out = FileChannel.open(Path.of("dst.bin"),
         StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
    long total = in.size();
    long transferred = 0;
    while (transferred < total) {
        transferred += in.transferTo(transferred, total - transferred, out);
    }
}

// Case 4: Random access to a binary database file (seeks, reads, writes)
// ANSWER: NIO FileChannel (seekable, readable+writable)
try (FileChannel fc = FileChannel.open(Path.of("db.bin"),
        StandardOpenOption.READ, StandardOpenOption.WRITE)) {
    fc.position(recordIndex * RECORD_SIZE);    // seek to record
    ByteBuffer record = ByteBuffer.allocate(RECORD_SIZE);
    fc.read(record);     // read record
    record.flip();
    int id = record.getInt();
    // Modify and write back:
    record.clear();
    record.putInt(newId);
    record.flip();
    fc.position(recordIndex * RECORD_SIZE);
    fc.write(record);
}

// Case 5: Server handling 10,000 concurrent HTTP connections
// ANSWER: NIO Selector or virtual threads (Java 21)
// Option A: NIO Selector (pre-Java 21, maximum scalability)
Selector selector = Selector.open();
// ... register channels, event loop as shown above

// Option B: Virtual threads (Java 21+, simplest for new code)
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    ServerSocket server = new ServerSocket(8080);
    while (true) {
        Socket client = server.accept();
        executor.submit(() -> handleClient(client));  // blocking I/O, virtual thread unmounts
    }
}

// Case 6: Watch a directory for file changes (CI/CD, hot reload)
// ANSWER: NIO.2 WatchService
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
    Path dir = Path.of("/conf");
    dir.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
    WatchKey key;
    while ((key = watcher.take()) != null) {
        key.pollEvents().forEach(e ->
            System.out.println("Changed: " + e.context())
        );
        key.reset();
    }
}

// ── Performance profile: when each NIO feature pays off ──────────────
// Buffers vs streams:
//   Buffered streams amortize system calls — good enough for most cases
//   NIO buffers add direct buffer option — eliminates heap-to-native copy
//   Break-even: only matters for >100MB/s throughput or latency < 1ms target

// FileChannel.transferTo:
//   Eliminates all user-space copies for file-to-file or file-to-socket
//   On Linux: maps to sendfile() — kernel copies between file descriptors
//   Speedup: 2-5x for large file copying vs buffered stream copy

// Memory-mapped I/O:
//   Best for random-access patterns on large files (>100MB)
//   Latency for first access to a page: 0.1ms (cold) to <1µs (hot)
//   Sequential large-file reads: comparable to transferTo
//   Bad fit: files that don't fit in virtual address space (>4GB on 32-bit JVMs)

// Selector-based non-blocking:
//   Needed when: threads > 1000 concurrent connections is unacceptable
//   Virtual threads (Java 21+) eliminate this need for most use cases
//   Selector still relevant for extreme scale (>100k connections/process)

Related Topics in Java NIO

Path
Path is the central interface in java.nio.file, introduced in Java 7 as part of NIO.2, representing a location in a file system as a sequence of path elements separated by a delimiter. Unlike java.io.File, Path is an interface backed by a pluggable FileSystem provider — the default provider maps to the OS file system, but ZIP archives, in-memory file systems, and remote file systems can provide their own Path implementations. Path is immutable and does not represent an open file or an existing file system entry; it is purely a path string with structured access and manipulation methods. Path provides substantially better semantics than File: path component access, resolution, relativization, normalization, and iteration over path elements are all first-class operations. Combined with the Files utility class, Path enables all file system operations with proper IOException-throwing semantics. This entry covers all Path construction methods, every path manipulation method in depth, path resolution and relativization with edge cases, normalization and toRealPath(), path iteration and watching, conversion to URI and File, comparison semantics, and cross-platform path handling.
Files Class
java.nio.file.Files is a utility class introduced in Java 7 containing over 70 static methods that perform all file system operations on Path objects: reading and writing file content, querying and setting file metadata, creating and deleting files and directories, copying and moving, directory listing and recursive traversal, checking permissions and existence, managing symbolic links, and obtaining channels and streams. Files is the NIO.2 replacement for the operations previously handled by java.io.File's mutation methods (which return boolean on failure) and for the InputStream/OutputStream factory pattern used with java.io streams. Every Files method throws IOException (or a subclass like NoSuchFileException, FileAlreadyExistsException, DirectoryNotEmptyException, or AccessDeniedException) rather than returning boolean, making error handling explicit and unambiguous. This entry covers every major category of Files method in depth, the complete set of StandardOpenOption and StandardCopyOption values, atomic operations and their filesystem-level semantics, bulk read/write convenience methods, the directory traversal API including Files.walk, Files.find, and Files.walkFileTree, permission and attribute manipulation, and the interplay between Files methods and the underlying OS system calls.
Channels
A channel in Java NIO is an open connection to an I/O entity — a file, a socket, a pipe — that can be read from, written to, or both. Channels differ from streams in being bidirectional (a single FileChannel can both read and write), operating exclusively with ByteBuffers rather than individual bytes, supporting seeking and position-based access (FileChannel), and being capable of non-blocking mode (network channels). The channel hierarchy descends from java.nio.channels.Channel at the root; ReadableByteChannel and WritableByteChannel define the reading and writing contracts; ByteChannel combines both; SeekableByteChannel adds position and size access; FileChannel extends SeekableByteChannel with file-specific operations including memory mapping, file locking, and zero-copy transfers. For network I/O, SocketChannel, ServerSocketChannel, and DatagramChannel support both blocking and non-blocking modes and are registered with a Selector for multiplexed event-driven I/O. This entry covers the complete FileChannel API including map(), lock(), transferTo(), and transferFrom(); the blocking and non-blocking network channels; the Selector and SelectionKey model; Pipe channels for inter-thread communication; and the AsynchronousChannel group for callback-based or Future-based I/O.
Buffers
A Buffer in Java NIO is a fixed-capacity, typed container for data that serves as the intermediary between application code and channels. Every I/O operation in NIO reads data into a buffer or writes data from a buffer — there is no byte-at-a-time API. The Buffer class hierarchy provides typed buffers for every Java primitive: ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, and DoubleBuffer. ByteBuffer is the foundation type because channels always operate in bytes; the other types are views over a ByteBuffer or are used for in-memory data processing. Every buffer has three state variables — position, limit, and capacity — that control where reads and writes occur and how much data is available. The flip(), clear(), compact(), rewind(), and mark()/reset() operations manipulate these state variables to implement the write-then-read and read-then-write patterns that buffer-based I/O requires. ByteBuffer can be backed by a Java heap array (allocate()) or by off-heap native memory (allocateDirect()), with significant performance implications for I/O operations. This entry covers all buffer state variables and their transitions, every buffer method in depth, heap vs direct buffer performance model, view buffers and byte order, the slice() and duplicate() operations, and the common patterns for buffer-based channel I/O.