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[]>.
- T make() { return new T(); }
- 1static <T> List<T> fill(int n, Supplier<T> make) {
- 2 List<T> out = new ArrayList<>();
- 3 for (int i = 0; i < n; i++) out.add(make.get());
- 4 return out;
- 5}
- 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).
all of them in one runnable place:
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());
}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:
| arrays | generics | |
|---|---|---|
| subtyping | covariant: a String[] is an Object[] | invariant: a List<String> is not a List<Object> |
| type info at run time | reified: the array knows it's a String[] | erased: the list is just a List |
| a wrong element fails | at the store, ArrayStoreException | at 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:
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.
}String[] Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
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:
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:):
$ 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:
@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"));
}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.
- no
new T(), nonew T[n], noinstanceof T, noT.class: there's no T at run time. - the workaround is always the same: have the caller pass a
Supplier, anIntFunction<T[]>or aClass<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.
@SafeVarargsis a promise, not a check.