Heap Pollution and SafeVarargs
Generics promise that a List<String> holds only strings. Heap pollution is when that promise is broken: a variable of type List<String> ends up pointing to a list that contains something else. Because of type erasure, the JVM can't notice when it happens. The problem surfaces later as a ClassCastException somewhere unexpected. It can only happen through code the compiler has already warned you about: raw types, unchecked casts, and generic varargs.
Polluting Through Varargs
A varargs parameter like List<String>... lists is really an array, List<String>[]. Arrays don't check generic types at runtime, so a method can store the wrong kind of list in it:
import java.util.ArrayList;
import java.util.List;
public class Poll {
static void pollute(List<String>... lists) {
Object[] array = lists;
array[0] = List.of(42);
String s = lists[0].get(0);
}
public static void main(String[] args) {
pollute(new ArrayList<>(List.of("Navika")));
}
}The compiler warns at both ends:
Poll.java:5: warning: [unchecked] Possible heap pollution from parameterized vararg type List<String>
static void pollute(List<String>... lists) {
^
Poll.java:12: warning: [unchecked] unchecked generic array creation for varargs parameter of type List<String>[]
pollute(new ArrayList<>(List.of("Navika")));
^
2 warningsAnd the program crashes on a line that contains no cast at all:
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
at Poll.pollute(Poll.java:8)
at Poll.main(Poll.java:12)How it works
listsis aList<String>[], but at runtime it's just an array ofList.- Viewing it as
Object[], the method stores aListcontaining42. The array accepts it because it's still aList. lists[0]is typedList<String>but now refers to a list of integers: the heap is polluted.lists[0].get(0)runs the compiler's hidden cast toStringand fails.
When @SafeVarargs Is Honest
Generic varargs are useful, so most of them are perfectly safe: List.of(...), Arrays.asList(...) and Set.of(...) all take T.... A method is safe if it only reads the varargs array and never stores into it or lets it escape. Such a method can be annotated @SafeVarargs, which tells the compiler "I promise" and removes the warnings for every caller:
import java.util.ArrayList;
import java.util.List;
public class SafeJoin {
@SafeVarargs
static <T> List<T> joinAll(List<? extends T>... lists) {
List<T> result = new ArrayList<>();
for (List<? extends T> list : lists) {
result.addAll(list);
}
return result;
}
public static void main(String[] args) {
List<String> team =
joinAll(List.of("Navika"), List.of("Srihitha", "Advitha"));
System.out.println(team);
}
}[Navika, Srihitha, Advitha]joinAll only reads each list, so @SafeVarargs is truthful here.
@SafeVarargs rules | |
|---|---|
| Allowed on | static methods, final instance methods, private instance methods, and constructors |
| Not allowed on | Methods that could be overridden, since a subclass could break the promise |
| Honest only if | The method never writes into the varargs array and never exposes it (for example by returning it) |
Common Mistakes
Ignoring "possible heap pollution" warnings. Each one is a place where the generic guarantee can break.
Returning or storing the varargs array. That lets callers see a T[] that might hold the wrong type.
Mixing raw types with generic ones. A raw List can put anything into a List<String> without any cast in sight.
Practice
- Easy: Remove
@SafeVarargsfromjoinAll, compile with-Xlint:unchecked, and read the warnings. - Harder: Write
static <T> T[] toArray(T... items) { return items; }and call it from a generic method. Explain why returning the varargs array is unsafe.
Key Takeaways
Was this tutorial helpful?