skip to content

Buffer Operations: flip/clear/rewind/compact

flip switches from writing to reading, clear resets for writing without erasing, compact preserves unread data, and rewind re-reads from the start. Forgetting flip before a read is the single most common NIO bug and the thing interviewers check.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

5

After writing data into a ByteBuffer, what must you do before reading it back, and why?

level: juniorimportance: must knowfreq 78%

answer

  1. One position cursor shared by read and write
  2. flip(): limit = position, position = 0
  3. Fill, flip, drain
  4. Skipping flip reads garbage up to capacity
  5. clear() before writing, flip() before reading

basics

~20 s

Call flip() before reading. Writing moves the position forward; flip() sets the limit to where you stopped writing and resets position to 0, so reads start at the beginning and stop where the data ends.

solid answer

~40 s

A NIO Buffer has one position cursor shared by reads and writes, plus a limit. While you write with put(), position advances. If you then call get() without flipping, you read garbage past your data and stop at capacity, not at your data's end. flip() does limit = position; position = 0. That makes position mark the start of valid data and limit mark its end, so subsequent get() calls walk exactly over what you wrote and hasRemaining() correctly reports when you are done. The canonical pipeline is: clear() (or allocate), channel.read()/put() to fill, flip(), then channel.write()/get() to drain. Forgetting flip() is the single most common NIO bug; symptoms are reading zeros or reading more than you wrote.

go deeper

for a junior

Knows the rule: flip() before reading what you just wrote, clear() before writing again. Can state flip sets position to 0.

for a middle

Explains the single shared position/limit model and exactly what flip() assigns (limit=position, position=0), and why skipping it reads past the data.

for a senior

Articulates the full fill/flip/drain and the clear-vs-compact choice for partial drains; recognizes the double-flip bug and contrasts flip with rewind and compact.

for a principal

Frames buffer-mode discipline as an API design trade-off (one cursor, manual mode switching) and can reason about it in zero-copy/channel pipelines, partial reads, and how mishandling causes subtle data-corruption bugs at scale.

## What a Buffer is In Java NIO (`java.nio`), a **Buffer** (e.g. `ByteBuffer`, `CharBuffer`) is a fixed-size container for primitive data backed by an array or off-heap memory. Unlike a plain array, a Buffer tracks state with three integer markers, always satisfying `0 <= mark <= position <= limit <= capacity`: - **capacity** — the total fixed size; set at allocation, never changes. - **position** — the index of the *next* element to be read or written. This single cursor is shared by both reading and writing. - **limit** — the index of the first element that should *not* be read or written; the boundary of the active region. - **mark** — a remembered position you can return to (optional). ## Why a single shared cursor causes the problem Because one `position` serves both modes, the buffer does not know whether you are currently filling it or draining it — *you* must tell it by adjusting the markers. When you allocate a buffer, `position = 0` and `limit = capacity`, i.e. it is in **write mode**: there is room from 0 up to capacity. Each `put(b)` (relative write) stores at `position` then does `position++`. So after writing N bytes, `position == N` and `limit` is still `capacity`. Now suppose you call `get()` (relative read) directly. `get()` reads at `position` then does `position++`. But `position` is N — past your data — and it will keep reading up to `limit == capacity`. You read leftover/zero bytes and far too many of them. This is the classic NIO failure. ## What flip() does `flip()` is exactly: ``` limit = position; // mark the END of the data you wrote position = 0; // go back to the START mark = -1; // discard any mark ``` Now `position = 0` (read from the beginning) and `limit = N` (stop at the end of valid data). The buffer is in **read mode**. Relative `get()` calls walk from 0 to N-1, and `hasRemaining()` (`position < limit`) becomes false exactly when you have read all N bytes. ## The canonical write-then-read pipeline ``` buf.clear(); // write mode: position=0, limit=capacity int n = channel.read(buf); // fills the buffer; advances position buf.flip(); // switch to read mode: limit=position, position=0 while (buf.hasRemaining()) { // drain process(buf.get()); } ``` Think of it as: **fill, flip, drain**. Every time you switch from putting data in to taking data out, you flip; every time you switch back to filling, you `clear()` (or `compact()` if unread data remains). ## Edge cases - Calling `flip()` twice in a row sets `limit = 0` (since position is now 0), making the buffer empty for reading — a real bug. - `flip()` does not erase data; it only repositions the markers. - Absolute `get(index)`/`put(index, b)` ignore and do not change `position`, so they are not affected by mode, but the common relative methods are.

  • What happens if you call flip() twice in a row?
    The first flip sets limit=position and position=0. The second flip sets limit=position=0, leaving an empty read region (nothing remaining) — a common bug that makes it look like there is no data.
  • What is the difference between flip() and rewind() before reading?
    flip() sets limit=position then position=0 — used after writing to bound reads to the data just written. rewind() only sets position=0 and leaves limit unchanged — used to re-read data already bounded by a prior flip().

saying these in an interview costs you the question

  • Thinking flip() erases or clears the data (it only moves markers)
  • Calling flip() twice and ending with limit=0
  • Believing reads automatically start at index 0 without flip()
  • Confusing flip() with rewind() (flip also sets limit)

context

open as a page

What is the difference between clear() and compact() on a ByteBuffer, and when would you choose compact()?

level: middleimportance: must knowfreq 62%

basics

~20 s

clear() resets the buffer to write mode as if empty (position=0, limit=capacity) but does not erase data — so any unread bytes are lost. compact() keeps unread bytes, moves them to the front, and positions the cursor right after them so you can append more. Use compact() when you have only partially drained the buffer.

open as a page

Explain mark(), reset(), rewind(), and remaining()/hasRemaining(), and how they relate to position and limit.

level: middleimportance: should knowfreq 40%

basics

~20 s

mark() saves the current position; reset() jumps position back to that saved mark. rewind() sets position to 0 (keeping limit) so you can re-read. remaining() returns limit minus position — the number of elements left — and hasRemaining() is true when remaining() > 0.

open as a page

What is the difference between relative and absolute get/put on a Buffer, and how do they interact with position and limit?

level: middleimportance: should knowfreq 45%

basics

~20 s

Relative get()/put(value) operate at the current position and then advance it by one, bounded by limit. Absolute get(index)/put(index, value) take an explicit index, do not read or change position, and are bounded by capacity-ish range checks. Relative is for streaming; absolute is for random access.

open as a page

Design a robust read loop that copies all bytes from one channel to another using a single ByteBuffer, and explain why each buffer operation is necessary and how partial reads/writes are handled.

level: seniorimportance: should knowfreq 33%

basics

~20 s

Loop: clear() the buffer, read() from the source into it, if read returns -1 stop, flip() it, then write() to the destination in a loop until no bytes remain. Because a single write() may not drain the whole buffer, you loop on hasRemaining(); flip and clear switch the buffer between filling and draining each iteration.

open as a page