Walk through the read(ByteBuffer) and write(ByteBuffer) semantics on a Channel, including buffer state transitions and partial transfers.
answer
- position / limit / capacity; remaining = limit - position
- read fills (position advances), write drains (position advances)
- flip(): limit=position, position=0 — fill→drain pivot
- clear() full reset vs compact() keep leftovers
- write can be partial → loop while hasRemaining()
basics
~20 sread(buffer) copies bytes from the channel into the buffer and moves the buffer's position forward by however many bytes were read. write(buffer) copies bytes from the buffer (between position and limit) out to the channel. Both return the count moved. You flip() the buffer to go from filling to draining, and may need to loop because a single call can transfer fewer bytes than you expect.
solid answer
~50 sA ByteBuffer tracks position, limit, and capacity. channel.read(buf) transfers up to buf.remaining() bytes from the channel into the buffer starting at position, then advances position by the count returned. A return of -1 means end-of-stream; 0 means nothing was read (normal in non-blocking mode). channel.write(buf) transfers the bytes between position and limit out to the channel and advances position by the count written. The crucial state transition is flip(): after filling a buffer you call flip() (sets limit=position, position=0) so the subsequent write drains exactly what you read. After writing, clear() (position=0, limit=capacity) resets for the next read, or compact() preserves any unwritten bytes. Partial transfers are real: in non-blocking mode write() may write fewer bytes than remaining, so you loop while buf.hasRemaining(). FileChannel in blocking mode generally transfers everything but you should still loop defensively.
code
java · 16 linesByteBuffer buf = ByteBuffer.allocate(1024);
// FILL phase
int n = channel.read(buf); // position advances by n; -1 = EOF
// PIVOT
buf.flip(); // limit=old position, position=0
// DRAIN phase (loop: write may be partial)
while (buf.hasRemaining()) {
channel.write(buf); // position advances by bytes written
}
// RESET
buf.compact(); // keep any leftover, ready to fill again
// or buf.clear(); if fully drainedgo deeper
Knows read fills the buffer and write empties it, and that flip() is needed in between.
Can recite position/limit/capacity, write a correct copy loop, handle the -1 sentinel, and explain clear vs compact.
Explains partial transfers from the OS-buffer perspective, loops writes correctly for non-blocking channels, and chooses compact vs clear deliberately.
Sets team idioms for buffer reuse and back-pressure handling, and reasons about how partial-transfer semantics interact with selector-driven event loops at scale.
## The ByteBuffer state model (you must know this cold) A `ByteBuffer` is a fixed array with three cursors: - **capacity** — total size, fixed at allocation. - **limit** — the first index you may *not* touch (the boundary of the active region). - **position** — the index of the next byte to be read or written. Invariant: `0 <= position <= limit <= capacity`. `remaining()` = `limit - position` (how many bytes are still available in the current operation). ## read(ByteBuffer) `int n = channel.read(buf)`: 1. The channel transfers bytes from its source into `buf`, starting at `buf.position()`, up to at most `buf.remaining()` bytes. 2. `buf.position` advances by `n` (the bytes actually read). 3. Return value: - `n > 0` — that many bytes were read. - `n == 0` — no bytes were read. **Legal**, and expected in non-blocking mode when the source has nothing ready right now. (In blocking mode this is rare.) - `n == -1` — **end-of-stream**: the source has no more bytes (file fully read, or the peer closed the socket). After `read`, the buffer is in *fill* state: position points just past the freshly read data, limit is still at capacity. ## flip() — the pivot between filling and draining To now *use* (write out / decode) the bytes you just read, you must read them from the **start**. `flip()` does exactly that: ``` limit = position; // mark how much data is there position = 0; // rewind to the beginning ``` Now `remaining()` = the number of bytes that were read. Skipping `flip()` is the single most common NIO bug — you'd write garbage from position-to-capacity instead of your data. ## write(ByteBuffer) `int n = channel.write(buf)`: 1. The channel transfers the bytes between `buf.position()` and `buf.limit()` out to its sink. 2. `buf.position` advances by `n` (the bytes actually written). 3. Return value is the count written; it can be **less than remaining()**, especially on a non-blocking `SocketChannel` whose send buffer is full (it may even return `0`). Because of partial writes you loop: ``` while (buf.hasRemaining()) { channel.write(buf); } ``` For a blocking `FileChannel`, `write` typically drains the whole buffer in one call, but defensive looping costs nothing and is correct everywhere. ## clear() vs compact() — resetting after a write - `clear()`: `position = 0; limit = capacity`. Use when the buffer was fully drained — start filling again from scratch. (Note: it does **not** erase data; it just resets cursors.) - `compact()`: copies any unread bytes (position..limit) to the front, then sets position after them and limit to capacity. Use when a partial write left leftover bytes you must keep for the next round. ## rewind() and mark()/reset() - `rewind()`: `position = 0` (limit unchanged) — re-read the same region. - `mark()` records position; `reset()` returns to it. ## A canonical, fully-correct copy loop ``` ByteBuffer buf = ByteBuffer.allocate(8192); while (in.read(buf) != -1) { buf.flip(); while (buf.hasRemaining()) out.write(buf); buf.clear(); } ``` ## Why partial transfers exist The OS owns finite kernel buffers. A non-blocking socket write only accepts what fits in the socket send buffer right now; the rest is your responsibility next time the channel is writable (which a Selector tells you). Reads are symmetric: a non-blocking read returns only what's currently available. Treating these counts as authoritative — and looping — is the heart of writing correct channel code.
- When would you use compact() instead of clear()?When a write only partially drained the buffer and unwritten bytes remain. compact() shifts those leftover bytes to the front so the next read appends after them, preserving the unflushed data; clear() would discard the boundary and overwrite them.
- Can read() return 0 on a blocking FileChannel?Only in edge cases (e.g. a zero-remaining buffer). In blocking mode a read normally blocks until at least one byte is available or returns -1 at EOF; a 0 return is the norm for non-blocking channels with no data ready.
saying these in an interview costs you the question
- Writing the buffer without flip() after a read
- Assuming write() always writes all remaining bytes (false in non-blocking mode)
- Treating read()==0 as end-of-stream (only -1 is EOF)
- Using clear() when a partial write left unflushed bytes (compact() is needed)
- Believing clear() zeroes the buffer contents (it only resets cursors)