skip to content

How would you design a generic varargs API so that heap pollution is impossible rather than merely asserted away?

level: principalimportance: nice to knowfreq 22%

answer

  1. Treat the varargs array as untrusted
  2. Copy to List<T> as the first statement
  3. Never return/store/write the array (no escape)
  4. Prefer Collection<? extends T> parameter over T...
  5. @SafeVarargs becomes honest, not load-bearing

basics

~10 s

Don't rely on @SafeVarargs promises. Inside the method, immediately copy the varargs into a List and work with that, never returning or storing the raw array. Then there is nothing that can be polluted.

solid answer

~50 s

The safest design treats the implicit varargs array as untrusted and never lets it influence external state. As the first action, copy the elements into a generic collection — e.g. `new ArrayList<>(Arrays.asList(args))` — and operate exclusively on that copy. Never return the array, never store it in a field, and never write into it. Because the only escaping object is a properly parameterized `List<T>` (not a non-reifiable array), there is no array whose static type overstates runtime guarantees, so heap pollution cannot occur — the safety is structural, not asserted. You can then apply @SafeVarargs honestly, since the method genuinely satisfies the rules. At the API boundary, prefer accepting a `Collection<? extends T>` or `List<T>` over varargs when the call ergonomics allow it, eliminating the implicit array entirely. Reserve generic varargs for read-only convenience overloads of such collection-based methods.

code

java · 12 lines
java
// PRINCIPAL-style design: structural safety, honest @SafeVarargs.

// Primary, hazard-free API: no implicit array at all.
static <T> List<T> combine(Collection<? extends T> items) {
    return new ArrayList<>(items);
}

// Convenience varargs overload: copies immediately, never lets 'args' escape.
@SafeVarargs
static <T> List<T> combine(T... args) {
    return combine(Arrays.asList(args)); // delegate; array never returned/stored/written
}

go deeper

for a junior

Recognizes that copying varargs into a List and not returning the array is safer than touching the array directly.

for a middle

Explains the two dangerous operations (store, escape) and that copying to a List<T> avoids both.

for a senior

Prescribes immediate copy-to-collection, no-escape discipline, and honest @SafeVarargs placement; can justify each rule from the erasure mechanism.

for a principal

Designs the API surface to make pollution structurally impossible (Collection parameter; varargs as a delegating convenience overload), sets review anti-patterns, and distinguishes asserted vs. designed-in safety.

## Why this question matters `@SafeVarargs` only *asserts* safety; it adds no enforcement. A principal-level concern is to design APIs whose safety is **structural** — guaranteed by construction — so future maintainers cannot accidentally reintroduce heap pollution. This means understanding the mechanism well enough to engineer it out. ## Recap of the mechanism (terms defined) - **Type erasure**: Java drops generic type arguments after compilation; at runtime `List<String>` is just `List`. So generic types are **non-reifiable** (their full type is not available at runtime). - **Arrays are reifiable**: a `String[]` knows its element type at runtime and rejects bad stores with `ArrayStoreException`. - **Generic varargs** force an array of a non-reifiable type to be created implicitly, using the *erased* element type. Its static type (e.g. `T[]`/`List<String>[]`) claims more than the runtime can enforce. - **Heap pollution**: a parameterized reference pointing to mismatched contents; it surfaces as a delayed `ClassCastException`. The two operations that turn this latent hazard into an actual bug are: **storing** into the implicit array, and **letting the array escape** the method. ## Design principles to make pollution impossible ### 1. Never let the implicit array escape The single most important rule. Do not `return args`, assign `args` to a field, or pass `args` (the whole array) to another method that might store into it or leak it. If the array reference never leaves the method, no external code can pollute or misread it. ### 2. Copy into a parameterized collection immediately Make the first statement convert the varargs to a generic collection you control: ```java List<T> safe = new ArrayList<>(Arrays.asList(args)); ``` From here on, you work with `List<T>`, which is a normal parameterized type whose element handling goes through type-checked methods. There is no raw array surface to pollute. Anything you return is a `List<T>`, not an array — so the dangerous non-reifiable-array-typed reference never crosses the boundary. ### 3. Prefer a collection parameter at the API boundary If callers can reasonably pass a collection, declare: ```java void process(Collection<? extends T> items) ``` instead of `T... items`. This removes the implicit array entirely — there is no varargs array to create, so the warning and the hazard simply do not exist. Use a varargs overload only as an ergonomic convenience that *delegates* to the collection-based method, copying first. ### 4. If you keep varargs, keep the body read-only A method that only iterates the elements (`for (T x : args) consume(x)`) and returns a fresh structure satisfies the safety rules. Only then apply `@SafeVarargs` — now it is an *honest* assertion of a genuinely safe body, on a `static`/`final`/`private` method or constructor. ## Anti-patterns to forbid in review - `return args;` (escape) - storing `args` in a field (escape) - `args[i] = ...` with a value whose runtime type differs (store/pollution) - blanket `@SuppressWarnings("unchecked")` to hide an unsafe escape ## The payoff When the only thing that crosses the method boundary is a properly parameterized `List<T>` (or no array exists at all because you took a `Collection`), the class of bugs is *eliminated*, not just silenced. The annotation, if present, documents intent rather than carrying the entire safety burden. This is the difference between *asserting* correctness and *designing* for it.

  • Why does taking a Collection<? extends T> instead of varargs remove the hazard entirely?
    Because no implicit array of a non-reifiable type is created at all. A Collection is a normal parameterized object with type-checked methods, so there is no array whose static type overstates runtime guarantees and nothing to pollute.
  • If you copy varargs into a List immediately, is @SafeVarargs still needed?
    The compiler still emits the warning because the implicit array is created on entry; @SafeVarargs (or a narrow @SuppressWarnings) silences it. But now it is an honest assertion, since the body is genuinely safe.

saying these in an interview costs you the question

  • Relying on @SafeVarargs as the safety mechanism instead of designing the hazard out
  • Returning the varargs array from a convenience overload
  • Believing copying to a List changes runtime behavior of callers (it only protects the implementation)
  • Assuming a Collection parameter is always worse ergonomically — it is often better and safer

context