Java Generics Errors Explained: Unchecked Casts and Idioms
Java generics compiler errors decoded: unchecked cast, generic array creation, capture of ?, name clash. Plus everyday idioms and when not to use generics.
- Course: Java study plan
- Module: Generics
- Kind: Lesson
- Reading time: 12 min
- Runtime: OpenJDK 18
What does an unchecked cast warning mean in Java?
An unchecked cast warning means the Java compiler cannot verify a cast involving generic types, such as (List<String>) obj, because type arguments are erased and only the raw class can be checked at run time. If the promise is wrong, a ClassCastException appears later. Prove the code safe, then suppress with @SuppressWarnings("unchecked") on the smallest scope and a comment.
Lesson
The previous lessons gave the rules; this one is the everyday texture: the idioms experienced Java code uses with generics, the compiler messages you will actually see and what each means, and the judgement calls — when to make something generic, when not to, how much wildcard is too much.
Idioms
Generic value containers. Optional<T>, Pair<A, B> (a record), Result<T> (a sealed interface with Ok<T>/Err<T> records) — small generic types that carry a value with a meaning. Records make these one-liners: record Pair<A, B>(A first, B second) {}.
Generic utility methods on a final class with a private constructor:
public final class Lists {
private Lists() {}
public static <T> List<T> reversed(List<T> xs) { … }
public static <T, R> List<R> map(List<T> xs, Function<? super T, ? extends R> f) { … }
public static <T extends Comparable<? super T>> T max(Collection<? extends T> xs) { … }
}
Typed empty results: List.of(), Collections.emptyList(), Optional.empty() infer their type from the target; return them instead of null.
Comparator building relies entirely on generic inference:
Comparator<Person> byAge = Comparator.comparingInt(Person::age);
Comparator<Person> byName = Comparator.comparing(Person::lastName, String.CASE_INSENSITIVE_ORDER);
people.sort(byAge.thenComparing(byName).reversed());
When inference fails in a chain (Comparator.comparing(Person::lastName).thenComparing(…) complaining that Object has no lastName), give the first lambda an explicit parameter type: Comparator.comparing((Person p) -> p.lastName()). The chain's first call has no target type to infer from.
Class tokens for run-time types: Class<T> parameters (Enum.valueOf(Day.class, s), EnumMap, fromJson(text, User.class)).
Bounded generic algorithms: <T extends Comparable<? super T>> on anything that orders; <T extends Number> on anything that computes.
The compiler messages, decoded
| Message | Meaning | Fix |
|---|---|---|
incompatible types: List<Integer> cannot be converted to List<Number> | Invariance | Use List<? extends Number> in the parameter |
unchecked call to add(E) as a member of the raw type List | A raw type is in use | Add the type arguments |
unchecked cast | (List<String>) obj — cannot be verified | Restructure; or contain with @SuppressWarnings("unchecked") and a justification |
generic array creation | new T[n] / new List<String>[n] | Object[] with cast, Array.newInstance, or a List |
non-static type variable T cannot be referenced from a static context | Class parameter used in a static member | Declare <T> on the static method |
inference variable T has incompatible bounds | Arguments imply conflicting Ts | Check the argument types; add a witness Util.<String>f(…) |
name clash: f(List<String>) and f(List<Integer>) have the same erasure | Overloading on type arguments | Rename one |
capture#1 of ? extends Number in a message | You tried to write into a ? extends list | Use ? super, a plain T, or a capture helper |
possible heap pollution from parameterized vararg type | Generic varargs | @SafeVarargs if the array is only read |
cannot select from a type variable (T.class, T.CONSTANT) | No static access through T | Pass a Class<T> |
Each message points at one rule from earlier lessons; recognising which one is most of the debugging.
@SuppressWarnings("unchecked") — the discipline
Unchecked warnings are the compiler admitting it cannot verify safety. Rules for suppressing:
- Prove to yourself the code is safe (usually: the only writes are typed).
- Suppress on the smallest element — a local variable declaration or a single method, never a class.
- Comment why it is safe.
@SuppressWarnings("unchecked")
T item = (T) items[--size]; // safe: only push(T) stores into items
Unexplained suppressions are where heap pollution hides.
How generic to be
- Make it generic when the same code would otherwise be duplicated per type, or when the element type is part of the contract (
Repository<T>,Cache<K, V>,Result<T>). - Do not make a class generic to look sophisticated: a
Config<T>whereTis alwaysStringis noise. - Wildcards on public parameters where callers benefit (
Collection<? extends T>); not on locals, fields or returns. - Bounds only when the body needs the bound's methods.
- A single type parameter covers most cases; three or more suggests a missing abstraction.
Generics and collections, together
The collections module (next) is generics in daily use: Map<String, List<Order>>, Set<? extends Shape>, Deque<Character>, Comparator<? super T>. The idioms there — computeIfAbsent(k, key -> new ArrayList<>()), List.copyOf, Collections.unmodifiableList — all depend on inference working as this module described.
Generics and lambdas, together
Functional interfaces are generic (Function<T, R>), so every lambda's type is a parameterised type inferred from context. When a stream pipeline's types go wrong, the first map is usually where inference lost the thread; typing the lambda parameter ((String s) -> …) restores it.
A worked example: a generic bounded cache
public final class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
public LruCache(int capacity) {
map = new LinkedHashMap<>(16, 0.75f, true) { // access-order
@Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; }
};
}
public V get(K key) { return map.get(key); }
public void put(K key, V value) { map.put(key, value); }
public V computeIfAbsent(K key, Function<? super K, ? extends V> loader) { return map.computeIfAbsent(key, loader); }
}
Two class parameters, an anonymous subclass that sees them (it is an inner class), and a PECS-correct Function<? super K, ? extends V> on the one public method that takes a function. This is what "generics in practice" looks like: a few lines, all of them mechanical once the rules are known.
Interview angle
- "How do you handle an unchecked cast you cannot avoid?" Prove it safe, suppress on the smallest scope, comment the reason.
- "Why does
Comparator.comparing(Person::name).thenComparing(…)sometimes fail to compile?" The first call has no target type; give the lambda an explicit parameter type. - "When would you not use generics?" When the type is always the same, or when the parameter would not be used in the body.
- "What does
capture of ?mean in an error?" You tried to write to a wildcard-typed collection; usesuperor a helper.
Key takeaways
- Idioms: generic records for values, static utility methods with bounds and PECS, typed empties, class tokens, comparator chains.
- Learn the error messages — each maps to one rule (invariance, erasure, statics, inference).
- Suppress unchecked warnings narrowly, with proof and a comment.
- Be as generic as the contract requires and no more; wildcards on API parameters only.
Common questions
How do you fix List<Integer> cannot be converted to List<Number>?
The error comes from invariance: a List<Integer> is not a List<Number>. If the method only reads the list, declare the parameter as List<? extends Number>, which accepts a list of any Number subtype.
How should you use @SuppressWarnings("unchecked")?
Only after proving the code safe — usually because every write is typed — and on the smallest element possible, a local variable declaration or a single method, never a whole class. Add a comment saying why it is safe; unexplained suppressions are where heap pollution hides.
Why does a Comparator.comparing chain fail to compile?
The first call in the chain has no target type, so a lambda such as p -> p.lastName() gets Object as its parameter type and lastName is not found. Give that lambda an explicit parameter type — (Person p) -> p.lastName() — and the chain compiles.
When should you not make a class generic?
When the type parameter would always be the same type — a Config<T> where T is always String — or would not be used in the body. Make code generic when it would otherwise be duplicated per type, or when the element type is part of the contract, as in Repository<T>.
What does "cannot select from a type variable" mean?
The code tried a static access through a type parameter, such as T.class, which erasure makes impossible. Pass a Class<T> parameter instead, as Enum.valueOf(Day.class, s) and EnumMap do.
Exercises
A generic LRU cache
Write class LruCache<K, V> backed by an access-ordered LinkedHashMap (constructor new LinkedHashMap<>(16, 0.75f, true)) whose anonymous subclass overrides removeEldestEntry to evict beyond a capacity. Provide V get(K key) (returns null if absent), void put(K key, V value), V computeIfAbsent(K key, Function<? super K, ? extends V> loader) and Set<K> keys() (the map's key set, in access order).
Read a capacity c, an integer n and n commands: put <k> <v>, get <k> (print the value or miss), load <k> (computeIfAbsent with the loader k -> k.toUpperCase(); print the value). After all commands print keys=<keys joined by ,>.
Example: capacity 2, then put a 1, put b 2, get a, put c 3, get b →
1
miss
keys=a,cargmax over any map
Write static <K, V extends Comparable<? super V>> K argmax(Map<K, V> m) returning the key with the largest value (the first met in iteration order on ties) and static <K, V extends Comparable<? super V>> List<K> keysAbove(Map<K, V> m, V threshold) returning keys whose value exceeds the threshold, in iteration order. Read n lines <name> <score> into a LinkedHashMap<String, Integer> and a threshold; print best=<argmax> and above=<keysAbove>.
Example: 3 then ann 70, bob 90, cy 90, then 75 →
best=bob
above=[bob, cy]In this module: Generics
← Type erasure and its consequences · Checkpoint — Generics →