skip to content

How does the capture-helper (capture-conversion) idiom let you implement a method like swap on a List<?>, and why is the helper necessary?

level: seniorimportance: should knowfreq 40%

answer

  1. Public List<?> wrapper -> private <T> List<T> helper
  2. Capture happens at the helper call
  3. Inside helper get()->T and set() accepts T
  4. Effective Java Item 31
  5. Needed because captured type can't be named inline

basics

~20 s

You write a public method taking List<?> and have it call a private generic helper method <T> that takes List<T>. Passing the wildcard list to the helper makes the compiler capture the unknown type as T, so inside the helper you can read and write elements consistently.

solid answer

~50 s

Operations like swapping elements need the compiler to know that what you read from a list can be written back to it. On a List<?> directly, get() returns Object and set() refuses an Object, so it won't compile. The fix is the capture-helper idiom: keep a clean public signature swap(List<?> list, int i, int j), and inside it call a private generic helper <T> void swapHelper(List<T> list, int i, int j). When you pass the wildcard list to the helper, the compiler performs capture conversion — it binds the unknown wildcard type to the helper's type parameter T. Inside the helper, list is a List<T>, so get() returns T and set() accepts T, and the read-then-write-back type-checks. The helper is necessary because you cannot name the captured type at the call site; only matching the wildcard against a real type parameter gives the compiler a name (T) to reason with.

code

java · 9 lines
java
public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j); // capture conversion happens here
}

private static <T> void swapHelper(List<T> list, int i, int j) {
    T tmp = list.get(i);          // get() returns T
    list.set(i, list.get(j));     // set() accepts T
    list.set(j, tmp);             // type-safe round-trip
}

go deeper

for a junior

Can recognize that swapping on List<?> won't compile directly and that a generic method fixes it.

for a middle

Writes the public-wrapper / private-generic-helper pair and explains that get/set now share type T.

for a senior

Explains precisely why the direct form fails and where capture occurs, and prefers the idiom over unchecked casts.

for a principal

Generalizes the pattern to any read-and-write wildcard operation, discusses API design tradeoffs of exposing vs hiding type parameters, and reviews team code for unsafe cast shortcuts.

## The motivating failure Suppose you want to swap two elements in place: ```java public static void swap(List<?> list, int i, int j) { list.set(i, list.set(j, list.get(i))); // does NOT compile } ``` `list.get(i)` on a `List<?>` returns `Object`. `list.set(j, ...)` expects the list's element type, which is unknown, so it won't accept an `Object`. The compiler complains even though the value plainly came from the same list and is therefore the right type. ## Why the direct form fails A `List<?>` has *some* element type, but the compiler refuses to write into it because it can't name that type to verify the argument. There's no syntax to say "the element type of this particular list," so you're stuck — unless you give the compiler a way to bind that unknown to a name. ## The idiom Delegate to a **private generic helper** whose type parameter captures the wildcard: ```java public static void swap(List<?> list, int i, int j) { swapHelper(list, i, j); // capture happens here } private static <T> void swapHelper(List<T> list, int i, int j) { T tmp = list.get(i); // get() now returns T list.set(i, list.get(j)); // set() accepts T list.set(j, tmp); // type-safe } ``` When `swap` calls `swapHelper(list, ...)`, the argument has type `List<?>` and the parameter is `List<T>`. The compiler applies **capture conversion**: it infers `T` to be the fresh captured type of the wildcard. Inside `swapHelper`, the list is a genuine `List<T>`, so `get` returns `T` and `set` accepts `T`; reading element i and writing it into slot j both type-check, because both are `T`. ## Why two methods? You *could* expose `swapHelper` directly with a `<T>` signature, but then callers would have to (or get to) supply/infer T. The wrapper keeps the public API clean (`List<?>`, no exposed type parameter) while the helper does the type-safe work. The helper is **necessary** because capture only happens when a wildcard meets a type parameter — there is no in-line way to name the captured type. The boundary between the two methods is exactly where capture occurs. ## Mental model - Public method: ergonomic, wildcard-typed, no exposed `<T>`. - Crossing into the helper: the compiler's chance to capture the unknown type into `T`. - Inside the helper: everything is a normal `List<T>`, so self-referential read/write is sound. This is the canonical pattern (Effective Java, Item 31) for any operation on a wildcard collection that must both read and write elements.

  • Could you fix swap with an @SuppressWarnings cast instead?
    You could cast to List<Object>, but that is an unchecked, unsafe cast that defeats type safety. The capture-helper idiom is fully type-safe and needs no suppression.
  • Where exactly does capture conversion occur in the idiom?
    At the call site where the wildcard-typed argument (List<?>) is bound to the helper's type parameter (List<T>) — the compiler infers T as the captured type there.

saying these in an interview costs you the question

  • Casting to List<Object> to make swap compile (unsafe)
  • Suppressing unchecked warnings instead of using the helper
  • Thinking the wrapper alone (no generic helper) is enough
  • Believing get() on List<?> returns the element type, not Object
  • Exposing <T> on the public API unnecessarily

context