skip to content
everything.generics.genericsLimitations

arrays, varargs and other limits

no new T(), no generic arrays, heap pollution and @SafeVarargs.

chapter
chapter 05.06
reading time
8 min read

erasure leaves generic code knowing nothing about T at run time, so a few things are simply impossible: no new T(), no new T[n], no instanceof T, no T.class, no generic exceptions. and type arguments must be references, so no List<int>.

the sharp edge is arrays. arrays are covariant and check their element type at run time, generics are invariant and erased. mix them (generic varargs do, secretly) and you get heap pollution: a List<String> holding something that isn't a string, and a ClassCastException somewhere else, later.

the picture

you know how java will happily let you write T everywhere in a class, and then refuses new T()? it feels arbitrary until you remember what's left of T at run time: nothing. a type that doesn't exist can't be constructed, can't be tested with instanceof, can't make an array of itself.

think of a recipe that says "bake it at the temperature on the packet", and then the packet has been thrown away (erasure). you can still follow every step that doesn't need the number. the moment a step needs the actual temperature, you're stuck, unless someone hands it to you. most of the workarounds below are exactly that: the caller hands over the thing the code can't recover, a Class<T>, a Supplier<T>, an IntFunction<T[]>.

Lim.java
  1. T make() { return new T(); }
error: unexpected type required: class found: type parameter T
because of erasurewhat to do instead
instead
  1. 1static <T> List<T> fill(int n, Supplier<T> make) {
  2. 2 List<T> out = new ArrayList<>();
  3. 3 for (int i = 0; i < n; i++) out.add(make.get());
  4. 4 return out;
  5. 5}
  6. 6fill(3, StringBuilder::new);

at run time there's no T to call a constructor on, it's erased to Object. so ask the caller for a way to make one: a Supplier<T> (usually a constructor reference).

pick a limitation. the red line is what you'd like to write and javac 25's real error for it. the box below is the usual workaround, which compiles.

all of them in one runnable place:

Main.java
static <T> List<T> fill(int n, Supplier<T> make) {     // instead of new T()
    List<T> out = new ArrayList<>();
    for (int i = 0; i < n; i++) out.add(make.get());
    return out;
}
 
static <T> T[] newArray(IntFunction<T[]> make, int n) {   // instead of new T[n]
    return make.apply(n);
}
 
void main() {
    List<StringBuilder> bufs = fill(2, StringBuilder::new);
    String[] slots = newArray(String[]::new, 3);
    IO.println(bufs.size() + " " + slots.length + " " + slots.getClass().getSimpleName());
 
    List<String> names = List.of("ada", "bob");
    Object[] a = names.toArray();
    String[] b = names.toArray(String[]::new);
    IO.println(a.getClass().getSimpleName() + " " + b.getClass().getSimpleName());
}
output
2 3 String[]
Object[] String[]

that last line is the whole problem in miniature. a List<String> can't hand you a String[] by itself, because it doesn't know it holds strings. plain toArray() returns an Object[]. to get a real String[], you pass in something that knows how to make one.

arrays play by different rules

arrays came first (java 1.0) and made two choices generics later reversed:

arraysgenerics
subtypingcovariant: a String[] is an Object[]invariant: a List<String> is not a List<Object>
type info at run timereified: the array knows it's a String[]erased: the list is just a List
a wrong element failsat the store, ArrayStoreExceptionat compile time

covariance means you can hold a String[] through an Object[] variable and try to store an Integer. the compiler can't stop that, so the array checks every single store at run time:

Main.java
void main() {
    String[] words = {"a", "b"};
    Object[] things = words;       // fine: String[] is an Object[]
    IO.println(things.getClass().getSimpleName());
    things[0] = 42;                // compiles. the array knows better.
}
output
String[]
Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
stack
main
words
things
heap
String[]knows it holds Strings
0
1
String
"a"
String
"b"
Integerrejected
42
two variables, one array. the arrows don't care that one variable says Object[]: the object itself is a String[], and it says so in its header. that's what lets it reject the Integer at the door.

now combine the two. an array of a generic type would be covariant like an array, but erased like a generic. its store check would only see List, never List<String>. step through what would happen:

stack
main
lists
heap
List[]
0
List
0
String
"ada"

List<String>[] lists = new List<String>[1]; pretend java allowed it. at run time the array is only a List[]: erasure took the String away.

step 1/4
step through it. this is why new List<String>[n] is illegal, and it's exactly what generic varargs do behind your back.

a variable of type List<String> pointing at something that isn't one is called heap pollution. the language bans generic array creation so you can't do this by accident. but there's one place java creates generic arrays for you anyway.

varargs and heap pollution

a varargs parameter T... xs is an array. call it with List<String> arguments and javac has to create a List<String>[] to pass them, the very thing you're not allowed to write. it does it anyway, and warns twice (with -Xlint:unchecked; without it you only get a Note:):

terminal
$ javac -Xlint:unchecked Va.java
Va.java:3: warning: [unchecked] Possible heap pollution from parameterized vararg type List<String>
    static void dangerous(List<String>... lists) {
Va.java:16: warning: [unchecked] unchecked generic array creation for varargs parameter of type List<String>[]
        dangerous(List.of("a"));

the first warning is for whoever writes the method, the second for every caller. if the method is actually safe, its author can say so with @SafeVarargs, and both warnings go away. it's a promise, though, not a check. here's what happens when the promise is a lie:

Main.java
@SafeVarargs // a promise javac takes on faith
static void firstUpper(List<String>... lists) {
    Object[] array = lists;          // arrays are covariant, so this compiles
    array[0] = List.of(42);          // a List is a List, so no ArrayStoreException
    IO.println(array.getClass().getSimpleName());
    String s = lists[0].get(0);      // the hidden cast to String fails here
    IO.println(s.toUpperCase());
}
 
void main() {
    firstUpper(List.of("ada"), List.of("bob"));
}
output
List[]
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')

a generic varargs method is safe if it does two things: it never stores anything into the array, and it never lets the array escape (returning it, or passing it to code that might store into it). reading the elements is fine. that's what List.of(E...), Arrays.asList(T...) and EnumSet.of do, and they're all marked @SafeVarargs in the JDK source.

quick check
where is @SafeVarargs allowed?
keylines
  • no new T(), no new T[n], no instanceof T, no T.class: there's no T at run time.
  • the workaround is always the same: have the caller pass a Supplier, an IntFunction<T[]> or a Class<T>.
  • no primitives as type arguments. List<Integer> boxes.
  • arrays are covariant and checked at run time. generics are invariant and checked at compile time.
  • generic varargs create generic arrays. safe only if you never store into them or leak them.
  • @SafeVarargs is a promise, not a check.