generics
items 26–33: raw types, wildcards, and typesafe containers.
- chapter
- chapter 16.04
- reading time
- 8 min read
generics move type errors from run time to compile time. items 26 to 33 are about not throwing that away: no raw types, take unchecked warnings seriously, prefer lists to arrays (they don't mix), and use wildcards so your APIs accept everything they safely can. producer extends, consumer super.
- item 26no raw typesdiamond, var
- item 27every unchecked warning is a possible ClassCastException
- item 28lists over arrays
- item 29make your types generic
- item 30make your methods generic
- item 31wildcards make APIs flexible
- item 32generics and varargs mix badly@SafeVarargs on private methods
- item 33Class objects as keys: typesafe heterogeneous containers
the rules
item 26 · no raw types
a raw type (List with no <...>) exists only so pre-2004 code still compiles. it switches off the type checks, so the bug surfaces far away from where it was made:
static void addDefaults(List list) { // raw: anything goes
list.add(42);
}
void main() {
List<String> names = new ArrayList<>(List.of("ada"));
addDefaults(names); // compiles, with a warning
for (String n : names) IO.println(n.toUpperCase());
}ADA
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.Stringthe cast that fails is one the compiler inserted in main, two calls away from the line that broke things. if you want "a list of anything", say so: List<?> (a list of some unknown type, which won't let you add to it) or List<Object> (a list that really can hold anything). raw types are only correct in class literals (List.class) and instanceof (o instanceof Set<?> is fine too).
item 27 · every unchecked warning is a possible ClassCastException
compile with -Xlint:unchecked and fix every warning you can. for the ones you can prove safe, suppress on the smallest possible scope (one local variable, not the whole class), and write down why:
class Ring<E> {
private final E[] slots;
Ring(int capacity) {
// safe: the array never leaves this class, and only Es go in
@SuppressWarnings("unchecked") E[] array = (E[]) new Object[capacity];
slots = array;
}
}a warning you suppress without proof is a ClassCastException you've scheduled for later, somewhere else.
item 28 · lists over arrays
arrays and generics disagree on two things. arrays are covariant (Long[] is an Object[]) and reified (they check element types at run time). generics are invariant (List<Long> is not a List<Object>) and erased (no type info at run time). so the same mistake fails at a different time:
void main() {
Object[] things = new Long[1];
IO.println("compiled fine");
things[0] = "hi";
}compiled fine Exception in thread "main" java.lang.ArrayStoreException: java.lang.String
List<Object> things =
new ArrayList<Long>();
// error: incompatible types:
// ArrayList<Long> cannot be
// converted to List<Object>that's also why new List<String>[10] is illegal: an array of a generic type would let you smuggle the wrong list in without either check catching it. reach for List<E> by default; use arrays for primitives and performance-critical internals. the details are in arrays, varargs and other limits.
item 29 · make your types generic
if your class stores Object and makes callers cast, make it generic. inside, you'll hit "can't create a new E[]": either keep an Object[] and cast on the way out (one suppressed warning on get), or create an E[] from an Object[] once in the constructor. the Stack<T> in item 7 takes the first route. callers get String s = stack.pop(), no cast.
item 30 · make your methods generic
static utility methods are the easiest win. declare the type parameters before the return type and let inference do the rest:
static <K, V> Map<V, K> invert(Map<K, V> m) {
Map<V, K> out = new HashMap<>();
m.forEach((k, v) -> out.put(v, k));
return out;
}
void main() {
Map<String, Integer> ports = Map.of("http", 80);
Map<Integer, String> byPort = invert(ports); // K and V inferred
IO.println(byPort.get(80));
}http
two related idioms: a generic singleton factory (one shared stateless object, handed out at any type, like Collections.emptyList() or Function.identity()), and a recursive bound like <T extends Comparable<T>>, which reads "a T that can be compared with other Ts".
wildcards
item 31 · wildcards make APIs flexible
invariance is safe but stiff: a method that takes Collection<Number> won't accept a List<Integer>. bounded wildcards loosen it exactly as far as is safe. the mnemonic is PECS: producer extends, consumer super.
without wildcards, each method takes exactly one type. a List<Integer> is not a List<Number>, so most calls fail.
static double sum(Collection<? extends Number> xs) { // produces Numbers for us
double total = 0;
for (Number n : xs) total += n.doubleValue();
return total;
}
static void fillZeros(List<? super Integer> out, int n) { // consumes Integers from us
for (int i = 0; i < n; i++) out.add(0);
}
void main() {
IO.println(sum(List.of(1, 2, 3)) + " " + sum(List.of(1.5, 2.5)));
List<Number> nums = new ArrayList<>();
List<Object> objs = new ArrayList<>();
fillZeros(nums, 2);
fillZeros(objs, 1);
IO.println(nums + " " + objs);
}6.0 4.0 [0, 0] [0]
two more rules of thumb. wildcards go on parameters, never on return types (they'd force wildcards onto your callers). and Comparable and Comparator are always consumers, which is why you see <T extends Comparable<? super T>> all over the JDK. try <T extends Comparable<T>> with a List<LocalDate>: it won't compile, because LocalDate is comparable to ChronoLocalDate (its supertype), not to LocalDate itself. the long version is in wildcards and PECS.
item 32 · generics and varargs mix badly
a varargs parameter is an array, and generic arrays can't be checked (item 28). so static <T> void m(T... xs) creates an array of an erased type: "heap pollution", a variable of a parameterized type pointing at something that isn't really that type. the method is safe only if it doesn't store into the array and doesn't let the array escape:
@SafeVarargs // a lie: the array escapes
static <T> T[] arrayOf(T... xs) { return xs; }
static <T> T[] twice(T x) { return arrayOf(x, x); } // T is erased here: this makes an Object[]
void main() {
Object[] fine = twice("hi"); // it really is an Object[]
IO.println(fine.length);
String[] both = twice("hi"); // the hidden cast to String[] fails
}2 Exception in thread "main" java.lang.ClassCastException: class [Ljava.lang.Object; cannot be cast to class [Ljava.lang.String; ([Ljava.lang.Object; and [Ljava.lang.String; are in module java.base of loader 'bootstrap')
if the method is safe, annotate it @SafeVarargs so callers don't get warnings (that's what List.of(E...) and Arrays.asList do). if you're not sure, take a List<T> instead of T....
item 33 · Class objects as keys: typesafe heterogeneous containers
normally the type parameters belong to the container: a Set<String> holds strings. sometimes you want a container that holds one value of each type, safely. the trick: put the type parameter on the key, and use the Class<T> object as that key.
final class TypedBag {
private final Map<Class<?>, Object> items = new HashMap<>();
<T> void put(Class<T> type, T value) {
items.put(Objects.requireNonNull(type), type.cast(value)); // checked even against raw callers
}
<T> T get(Class<T> type) {
return type.cast(items.get(type)); // a checked cast, no warning
}
}
void main() {
var bag = new TypedBag();
bag.put(String.class, "java");
bag.put(Integer.class, 25);
bag.put(Class.class, Thread.class);
String s = bag.get(String.class); // no cast at the call site
int n = bag.get(Integer.class);
Class<?> c = bag.get(Class.class);
IO.println(s + " " + n + " " + c.getSimpleName());
}java 25 Thread
type.cast is what keeps it honest.the limits: Class objects can't express List<String> (there's only List.class), and the JDK uses the same pattern in Class.cast, annotations (getAnnotation(Class<A>)), and ServiceLoader.load(Class<S>). more on what survives erasure in generics at runtime.
- raw types switch the compiler off. use
List<?>orList<Object>instead. - every unchecked warning is a future ClassCastException. fix it, or prove it and suppress it narrowly.
- arrays fail at run time, generics fail at compile time. prefer lists.
- PECS: read from
? extends T, write into? super T. never wildcards in return types. - generic varargs are safe only if the array never escapes and is never written.
Class<T>as a map key gives you a typesafe bag of one value per type.