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.
answer
- clear -> read -> flip -> write-loop -> repeat
- read() == -1 means EOF; break
- write() may be partial: loop on hasRemaining()
- flip before draining, clear/compact before filling
- non-blocking: read/write can return 0 (would block)
basics
~20 sLoop: 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.
solid answer
~50 sThe correct copy loop is: clear() to enter write mode, channel.read(buf) to fill (returns -1 at end of stream, 0 if nothing available on a non-blocking channel), flip() to switch to read mode bounding reads to what arrived, then an inner while(buf.hasRemaining()) dst.write(buf) loop because a single write may not consume the whole buffer (especially on non-blocking or back-pressured channels). After draining, loop back to clear() and read again. Each op is load-bearing: without clear() the second read would have no room; without flip() write() would send from the wrong region; without the inner write loop you'd silently drop bytes on a partial write. If you might leave unread bytes (you won't here since you fully drain), you'd use compact() instead of clear(). For non-blocking channels you must also handle read()==0 and write()==0 by yielding to a selector rather than spinning.
go deeper
Can recite the clear/read/flip/write order but may miss partial-write looping or EOF handling.
Writes a correct blocking copy loop with flip/clear and an EOF check, and explains why flip and clear are needed.
Handles partial writes with an inner hasRemaining() loop, chooses clear vs compact correctly, and distinguishes read()==0 from -1.
Designs the non-blocking/selector variant with back-pressure handling, sizing and pooling of buffers, direct vs heap buffer trade-offs, and reasons about throughput, GC, and zero-copy alternatives (transferTo/transferFrom).
## Goal Copy every byte from a source `ReadableByteChannel` to a destination `WritableByteChannel` using one reusable `ByteBuffer`. This exercises the full buffer-operation lifecycle and the realities of partial I/O. ## Background: channels and partial transfers A **channel** is NIO's connection to an I/O source/sink (file, socket). `channel.read(buf)` pulls bytes into the buffer's free space and returns the count read (>=0), or **-1** at end of stream. `channel.write(buf)` pushes bytes from the buffer's active region and returns the count written. Crucially, **neither is guaranteed to transfer everything in one call**: a socket may accept only part of your data (back-pressure), and a non-blocking channel may return **0** when nothing can be done right now. Correct code never assumes a single call moved all the bytes. ## The buffer lifecycle in the loop Each iteration alternates between *filling* (write mode) and *draining* (read mode): 1. **clear()** — enter write mode (`position=0, limit=capacity`) so `read()` has the whole buffer to fill. (It does not erase data; we have fully drained last round, so that's fine.) 2. **read(buf)** — fills from position toward limit, advancing position by the count read. If it returns **-1**, the source is exhausted: break. 3. **flip()** — switch to read mode (`limit=position, position=0`) so the next writes drain exactly the bytes just read, no more. 4. **inner write loop** — `while (buf.hasRemaining()) dst.write(buf);` — because one `write()` may drain only part of the buffer, we loop until `remaining()` is 0. Each `write()` advances position by what it wrote. 5. back to step 1. ## Reference implementation (blocking channels) ```java static long copy(ReadableByteChannel src, WritableByteChannel dst) throws IOException { ByteBuffer buf = ByteBuffer.allocate(16 * 1024); long total = 0; while (src.read(buf) != -1) { // fill; -1 => EOF buf.flip(); // switch to drain mode while (buf.hasRemaining()) { // partial-write safe total += dst.write(buf); } buf.clear(); // reset to fill mode (fully drained) } return total; } ``` Note: here `src.read(buf)` is called with the buffer already in write mode. After the first iteration, `clear()` restores write mode before looping, so the structure stays correct. (Some prefer an explicit `buf.clear()` at the top of the loop; both work as long as the buffer is in write mode before each read.) ## Why each piece matters - **No clear()/write-mode reset** → the second `read()` sees `remaining()==0` (no room) and reads 0 forever. - **No flip()** → `write()` drains from the current position to capacity, sending garbage/zeros and the wrong length. - **No inner write loop** → on a partial write you silently lose the bytes `write()` didn't take. - **Ignoring -1** → infinite loop at end of stream. ## Partial drains: clear vs compact Here we fully drain (inner loop runs until `hasRemaining()` is false), so **clear()** is correct. If instead you consumed only complete records and left a tail (e.g. a framed protocol), you'd replace `clear()` with **compact()** to preserve the leftover and append the next read behind it. ## Non-blocking channels With a non-blocking channel registered on a Selector, `read()` and `write()` can return **0** (would block). You must not spin: on a short `write()`, keep the buffer (don't clear), register interest in OP_WRITE, and resume when the selector reports writability. The same care applies to `read()==0`. The buffer-operation discipline (flip before draining, compact to keep unfinished data) becomes essential because data arrives in arbitrary chunks across many selector callbacks. ## Takeaways - Alternate write-mode (fill) and read-mode (drain) via clear/compact and flip every iteration. - Always loop writes until `hasRemaining()` is false — partial transfers are normal. - Use compact() instead of clear() whenever unread bytes may remain. - Honor -1 (EOF) and, for non-blocking, 0 (would block).
- Why must the write() be inside a loop on hasRemaining() rather than a single call?channel.write() returns how many bytes it actually wrote, which can be fewer than remaining() due to socket back-pressure or non-blocking semantics. Looping until hasRemaining() is false ensures the whole buffer is drained before refilling, preventing silent data loss.
- When would you replace clear() with compact() in this loop?When the inner loop intentionally stops before fully draining — e.g. you consume only complete protocol frames and a partial frame remains. compact() preserves that leftover at the front so the next read appends behind it, whereas clear() would discard it.
saying these in an interview costs you the question
- Assuming a single write() drains the whole buffer
- Omitting flip() between read and write
- Using clear() when unread bytes remain (should be compact())
- Spinning on read()==0/write()==0 instead of using a selector for non-blocking channels
- Treating read()==0 the same as -1 (EOF)