skip to content

What hazards arise when combining varargs with generics, and what is @SafeVarargs for?

level: seniorimportance: nice to knowfreq 30%

answer

  1. Varargs array + erased generics = generic array = warning
  2. Heap pollution: generic var points to wrong generic type
  3. Safe iff: read-only, no store, no leak of the array
  4. @SafeVarargs = unchecked promise, suppresses caller warnings
  5. Only on static / final / private / constructor (non-overridable)

basics

~20 s

Mixing generics with varargs creates a generic array under the hood, which Java can't fully type-check, so the compiler warns about possible 'heap pollution.' If the method only reads the array safely, you annotate it @SafeVarargs to suppress the warning.

solid answer

~50 s

A varargs parameter becomes an array, but you can't create a true generic array in Java because generics are erased. So a method like List<String>... actually receives a List[] whose element type can't be verified at runtime — this gap is called heap pollution: a variable of a generic type may reference an object of a different generic type, risking a ClassCastException later. The compiler emits an 'unchecked generic array creation' warning at both the declaration and each call site. If the method only iterates and reads the varargs array (never stores into it, and never lets the array reference escape so a caller can corrupt it), it's actually safe, and you assert that with @SafeVarargs, which silences the warnings. @SafeVarargs may only go on methods that can't be overridden — static, final, or private instance methods (and constructors) — because overriding could break the safety guarantee. Misusing it on a method that does write to or leak the array can hide a real bug.

code

java · 12 lines
java
// UNSAFE: leaks the varargs array, enabling heap pollution
static <T> T[] toArray(T... args) { return args; }   // do NOT @SafeVarargs this
static <T> T[] pickTwo(T a, T b) { return toArray(a, b); } // runtime Object[]
// String[] s = pickTwo("x", "y"); -> ClassCastException

// SAFE: read-only, never stores or leaks the array
@SafeVarargs
static <T> List<T> listOf(T... elems) {
    List<T> out = new ArrayList<>();
    for (T e : elems) out.add(e);   // only reads from elems
    return out;
}

go deeper

for a junior

Generally not expected; awareness that combining generics and varargs produces a compiler warning is enough.

for a middle

Knows the 'unchecked generic array creation' warning appears and that @SafeVarargs silences it, even if the deeper reasoning is fuzzy.

for a senior

Explains heap pollution from erasure-driven generic arrays, the read-only/no-store/no-leak safety conditions, and that @SafeVarargs is an unverified promise restricted to non-overridable methods.

for a principal

Sets API guidelines: prefer List<T> over generic varargs, audits @SafeVarargs usages for genuine safety, and articulates the reified-arrays-vs-erased-generics unsoundness underlying the rule.

## Background you need first Two Java facts collide here: 1. **A varargs parameter is an array** (`T... x` becomes `T[] x`). 2. **Generics are erased at runtime** — `List<String>` and `List<Integer>` are both just `List` once compiled. Java therefore **forbids creating arrays of a parameterized type**: `new List<String>[10]` is a compile error, because arrays are *reified* (they remember their element type at runtime) while generics are not, and the combination is unsound. ## The conflict So what happens with `static <T> void f(List<T>... lists)`? The compiler *must* create an array to hold the varargs — but it's a generic array, which is normally illegal. The compiler allows it as a special case but **flags it with an 'unchecked generic array creation' warning**, because the resulting `List[]` cannot fully police its element types at runtime. ## Heap pollution defined **Heap pollution** is the situation where a variable of a parameterized type (e.g. `List<String>`) refers to an object that is *not* of that parameterized type (e.g. a `List<Integer>`). Because erasure removed the type tag, the JVM can't catch this at the point of assignment; instead you get a surprising `ClassCastException` *later*, when the value is finally used and an implicit cast fails. Generic varargs is a prime way to introduce it. The dangerous pattern is when the method **stores the varargs array somewhere or returns it / passes it out**, letting code observe or corrupt it with the wrong type: ```java @SafeVarargs // <-- WRONG: this method is NOT safe static <T> T[] toArray(T... args) { return args; } // leaks the array static <T> T[] pickTwo(T a, T b) { return toArray(a, b); // creates an Object[] at runtime } String[] s = pickTwo("x", "y"); // ClassCastException: Object[] is not String[] ``` ## When generic varargs IS safe A generic varargs method is safe when it: - **Only reads** from the varargs array (iterates, indexes for reading), and - **Never stores** anything into the array, and - **Never lets the array reference escape** (doesn't return it or pass it to untrusted code that could store the wrong type). Methods like `List.of`, `Arrays.asList`, `Collections.addAll`, and `Stream.of` satisfy this — they consume the elements without leaking the array. ## @SafeVarargs `@SafeVarargs` is your **promise to the compiler** that the method is safe despite the unchecked warning, and it suppresses the warning **at the declaration and at every call site** (the call-site suppression is the real win — otherwise every caller sees the warning). Constraints: - It may only be applied to methods that **cannot be overridden**: `static` methods, `final` instance methods, **`private` instance methods** (Java 9+), and **constructors**. A non-final, non-static, non-private instance method is rejected, because a subclass override could violate the safety contract. - It is an **assertion you must justify** — the compiler does not verify the safety; it trusts you. Applying it to an unsafe method (like the leaking `toArray` above) silences the very warning that was protecting you. ## Practical guidance - Prefer a `List<T>` parameter over `T...` for generic APIs when you can — it sidesteps the whole issue (this is Effective Java's advice). - If you do expose generic varargs and the method is genuinely read-only, annotate it `@SafeVarargs` so callers aren't spammed with warnings. - Never annotate a method that writes to or leaks the varargs array. ## Summary Generic varargs forces an unsound generic array; the compiler warns about heap pollution. Read-only methods are safe and should carry `@SafeVarargs` (allowed only on non-overridable methods). The annotation suppresses the warning at declaration and call sites but is an unverified promise — use it only when the safety conditions truly hold.

  • Why can't @SafeVarargs go on a public, non-final instance method?
    Because such a method can be overridden, and an override could implement unsafe behavior (storing or leaking the array) while inheriting the 'safe' promise. Restricting the annotation to static, final, private, and constructors guarantees the implementation the compiler trusts can't be replaced.
  • What's the recommended alternative to a generic varargs parameter?
    Accept a List<T> (or other collection) instead of T... where practical. It avoids generic array creation and heap pollution entirely, at the cost of callers wrapping arguments in List.of(...). Effective Java recommends this when the varargs convenience isn't essential.

saying these in an interview costs you the question

  • Slapping @SafeVarargs on a method that returns or stores the varargs array
  • Thinking @SafeVarargs makes the code safe (it only suppresses warnings)
  • Claiming you can create new List<String>[] arrays freely
  • Putting @SafeVarargs on a non-final public instance method (won't compile)
  • Confusing heap pollution with a heap memory leak — it's a type-safety hole, not retained memory

context