skip to content

Buffer Model: capacity/limit/position/mark

A Buffer is a fixed-size primitive container tracked by four markers under the invariant mark <= position <= limit <= capacity. Understanding those markers is the prerequisite for every buffer operation question that follows.

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

questions

5

What is a Java NIO Buffer, and what do its four state markers (capacity, limit, position, mark) mean?

level: juniorimportance: must knowfreq 70%

answer

  1. 0 <= mark <= position <= limit <= capacity
  2. position = next slot, limit = the wall you can't cross
  3. capacity is fixed at allocation; never changes
  4. mark is a bookmark; reset() returns to it
  5. remaining() = limit - position

basics

~20 s

A Buffer is a fixed-size box that holds one kind of primitive value (like bytes). It tracks where you are with four numbers: capacity (total size), limit (how far you can go), position (the next slot to read/write), and mark (a saved spot you can jump back to).

solid answer

~40 s

A java.nio.Buffer is a fixed-capacity container for a single primitive type (ByteBuffer, IntBuffer, etc.). Its cursor state is four ints obeying 0 <= mark <= position <= limit <= capacity. Capacity is the total number of elements, fixed at allocation. Position is the index of the next element to be read or written; it advances on every get/put. Limit is the first index you may NOT touch — the boundary of accessible data. Mark is an optional remembered position you set with mark() and return to with reset(). The whole API works because reads and writes both move position toward limit, so the same object serves both phases as long as you reset limit and position correctly between them. Operations like flip(), clear(), and rewind() just reposition these markers.

go deeper

for a junior

Can name all four markers and state that capacity is fixed and position is the next slot to read/write.

for a middle

States the full invariant 0 <= mark <= position <= limit <= capacity and explains why both position and limit are needed for the fill-then-drain pattern.

for a senior

Connects the markers to flip/clear/rewind/compact, knows remaining()/hasRemaining(), and the exception behavior at boundaries.

for a principal

Reasons about the model's design trade-offs — single mutable cursor object, no thread safety, why fixed capacity simplifies native-memory mapping, and how this informs API design for zero-copy I/O.

## What a Buffer is In classic Java I/O you read and write through *streams* — you hand bytes to an `OutputStream` or pull them from an `InputStream`, one direction at a time, with the stream managing its own internal buffering invisibly. Java NIO (New I/O, package `java.nio`, since Java 1.4) inverts this: you work with an explicit, in-memory container called a **Buffer**, and channels move data between that buffer and the outside world (a file, a socket). A **Buffer** is a **fixed-size container for a single primitive type**. "Fixed-size" means once you allocate it, its total capacity never changes. "Single primitive type" means there is a separate subclass per primitive: `ByteBuffer`, `CharBuffer`, `IntBuffer`, `LongBuffer`, `FloatBuffer`, `DoubleBuffer`, `ShortBuffer` (and `MappedByteBuffer`). `ByteBuffer` is the one you use most, because channels read and write bytes. ## The four markers A buffer is conceptually an array plus four integer cursors. They always satisfy this invariant: ``` 0 <= mark <= position <= limit <= capacity ``` Let me define each, from the outside in: - **capacity** — the total number of elements the buffer can hold. Set once at allocation (`ByteBuffer.allocate(64)` → capacity 64) and **never changes**. It is the hard ceiling for every other marker. - **limit** — the index of the *first element you are NOT allowed to read or write*. Everything from `position` up to (but not including) `limit` is the accessible region. Initially `limit == capacity` (you may use the whole buffer). - **position** — the index of the *next* element a `get()` or `put()` will touch. It starts at 0 and **advances by one each time you read or write one element** (or by N for a bulk operation). When `position == limit`, there is nothing left in the current region. - **mark** — an *optional* bookmark. It is undefined ("unset") until you call `mark()`, which records the current `position`. Later, `reset()` sets `position` back to that marked value. The mark is discarded (becomes undefined) if `position` or `limit` is ever moved below it. ## Why limit and position both exist The genius (and the trap) of the model is that **the same buffer is used for both writing and reading**, and the markers tell the buffer which phase it is in. - While **filling** the buffer (e.g. `channel.read(buf)` writes data into it), `position` marks how much you have written and `limit` stays at capacity. - Then you **flip** with `flip()`: it sets `limit = position` ("the data ends here") and `position = 0` ("start reading from the front"). Now the accessible region is exactly the data you just wrote. - While **draining** the buffer (e.g. `channel.write(buf)` reads data out of it), `position` advances through the data until it hits `limit`. - `clear()` resets to `position = 0, limit = capacity` (ready to fill again — it does NOT erase the bytes). `rewind()` sets `position = 0` but leaves `limit` (re-read the same data). `compact()` moves unread bytes to the front and positions you to keep filling. ## remaining() and hasRemaining() `remaining()` returns `limit - position` — the number of elements still accessible. `hasRemaining()` is `position < limit`. These are how you loop over a buffer without ever touching the markers by hand. ## Edge cases and rules - Setting `limit` below the current `position` *also* drops `position` down to the new limit. Setting `position` (or `limit`) below `mark` discards the mark. - `reset()` throws `InvalidMarkException` if no mark is set. - Reading past `limit` (a relative `get()` when `!hasRemaining()`) throws `BufferUnderflowException`; writing past it throws `BufferOverflowException`. Absolute `get(index)`/`put(index, v)` ignore `position`/`limit` but still bounds-check against capacity (throwing `IndexOutOfBoundsException`). - Buffers are **not thread-safe**; the markers are plain mutable state. Once you internalize the invariant `0 <= mark <= position <= limit <= capacity` and the rule "position is the next slot, limit is the wall," every buffer method becomes predictable.

  • What does remaining() return, and in terms of which markers?
    It returns limit - position: the number of elements between the current position and the limit, i.e. how many you can still read or write.
  • What happens to the mark if you set the limit below it?
    The mark is discarded (becomes undefined). Any later reset() before re-marking throws InvalidMarkException.

saying these in an interview costs you the question

  • Thinking capacity can grow — buffers are fixed-size
  • Confusing limit with capacity (limit moves during read phase; capacity never does)
  • Believing clear() zeroes the data (it only resets markers)
  • Saying position points at the last element used (it points at the NEXT slot)
  • Assuming a mark always exists (it is undefined until mark() is called)

context

open as a page

Explain flip(), clear(), rewind(), and compact() in terms of how each moves the Buffer's position, limit, and mark.

level: middleimportance: must knowfreq 65%

basics

~20 s

flip() gets a buffer ready to read what you just wrote: it sets limit to the current position, then position to 0. clear() gets it ready to write again: position 0, limit at capacity (data is left untouched). rewind() sets position to 0 to re-read. compact() keeps the unread bytes and lets you keep writing.

open as a page

How do mark() and reset() work on a Buffer, and what edge cases govern the mark?

level: middleimportance: should knowfreq 40%

basics

~20 s

mark() saves the current position so you can come back to it. reset() moves position back to that saved spot. If you call reset() without ever calling mark(), or after the mark was wiped out, you get an InvalidMarkException.

open as a page

What is the difference between a direct ByteBuffer and a heap (non-direct) ByteBuffer, and when would you choose each?

level: seniorimportance: should knowfreq 55%

basics

~20 s

A heap buffer stores its bytes in a normal Java array on the garbage-collected heap. A direct buffer stores them in native (off-heap) memory the OS can access directly. Direct buffers make channel I/O faster because the JVM can skip an extra copy, but they cost more to allocate and free.

open as a page

What do slice(), duplicate(), and asReadOnlyBuffer() create, and how do their markers and backing storage relate to the original buffer?

level: seniorimportance: should knowfreq 35%

basics

~20 s

All three make a new buffer that shares the same underlying data as the original (a window onto it) but has its own independent position, limit, and mark. duplicate() mirrors the whole buffer, slice() exposes only the part from position to limit, and asReadOnlyBuffer() makes a view you can read but not write.

open as a page