Advanced Generics
AdvancedAdvanced generics covers wildcards, bounded type parameters, PECS, type erasure effects, and generic methods for writing flexible, reusable APIs.
Overview
Java generics provide compile-time type safety without runtime overhead — generic types are erased to their bounds at compile time. Advanced generics involve: bounded wildcards (? extends T, ? super T), the PECS rule (Producer Extends, Consumer Super), recursive type bounds, generic methods, and working around type erasure with TypeToken patterns. Mastering these is essential for writing reusable library and framework code.
Wildcards and Bounded Type Parameters
Unbounded wildcard <?> means "any type". Upper-bounded <? extends T> means T or any subtype — read-only (producer). Lower-bounded <? super T> means T or any supertype — write-allowed (consumer).
The PECS rule (Producer Extends, Consumer Super): use extends when you only read from a structure; use super when you only write to it.
// Upper bounded — read from (producer)
public double sumList(List<? extends Number> list) {
double sum = 0;
for (Number n : list) sum += n.doubleValue(); // can READ
// list.add(1.5); // COMPILE ERROR — can't write
return sum;
}
sumList(new ArrayList<Integer>()); // works
sumList(new ArrayList<Double>()); // works
// Lower bounded — write to (consumer)
public void addNumbers(List<? super Integer> list) {
list.add(1); // can WRITE Integer or subtype
list.add(2);
// Integer i = list.get(0); // COMPILE ERROR — can only get Object
}
addNumbers(new ArrayList<Integer>()); // works
addNumbers(new ArrayList<Number>()); // works
addNumbers(new ArrayList<Object>()); // works
// PECS in Collections.copy
// src is producer (we read from it) → extends
// dest is consumer (we write to it) → super
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T t : src) dest.add(t);
}Generic Methods and Recursive Bounds
Generic methods declare their type parameters in angle brackets before the return type. This allows the method's type parameter to be different from the class's.
Recursive type bounds: <T extends Comparable<T>> restricts T to types that can be compared to themselves — used in min/max, sort, and BST implementations.
// Generic method — type parameter per method
public static <T> List<T> repeat(T element, int times) {
List<T> result = new ArrayList<>(times);
for (int i = 0; i < times; i++) result.add(element);
return result;
}
List<String> strs = repeat("hello", 3); // inferred as String
List<Integer> nums = repeat(42, 5); // inferred as Integer
// Recursive type bound — T must be comparable to itself
public static <T extends Comparable<T>> T max(List<T> list) {
if (list.isEmpty()) throw new NoSuchElementException();
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) max = item;
}
return max;
}
max(List.of(3, 1, 4, 1, 5)); // Integer
max(List.of("b", "a", "c")); // String
// Multiple bounds
public <T extends Serializable & Comparable<T>> void process(T item) {
// T must implement both Serializable AND Comparable<T>
}Type Erasure and Workarounds
At runtime, generic type parameters are erased to their bounds (or Object if unbounded). This means List<String> and List<Integer> are the same class at runtime.
Workarounds: pass Class<T> as a token, use TypeReference (Jackson/Gson pattern), or capture type via anonymous subclass.
// Type erasure — same class at runtime
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
System.out.println(strings.getClass() == ints.getClass()); // true
// Can't do: new T(), instanceof List<String>, T[].class
// Workaround 1: pass Class<T>
public <T> T fromJson(String json, Class<T> type) {
return objectMapper.readValue(json, type);
}
User user = fromJson(json, User.class); // explicit token
// Workaround 2: TypeReference — anonymous subclass captures type
public <T> T fromJson(String json, TypeReference<T> ref) {
return objectMapper.readValue(json, ref);
}
List<User> users = fromJson(json, new TypeReference<List<User>>() {});
// Anonymous subclass retains generic type info via getGenericSuperclass()
// Workaround 3: unchecked cast with suppression
@SuppressWarnings("unchecked")
public <T> T readAttribute(Map<String, Object> map, String key) {
return (T) map.get(key); // erased, but caller knows the type
}Key Points to Remember
- PECS: Producer Extends (read), Consumer Super (write).
- <? extends T> allows reading as T; <? super T> allows writing T into the structure.
- Recursive bounds <T extends Comparable<T>> constrain T to self-comparable types.
- Type erasure: List<String> and List<Integer> are the same class at runtime.
- Workarounds for erasure: Class<T> token, TypeReference anonymous subclass, or @SuppressWarnings("unchecked") cast.
Practice Advanced Generics in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhat is the PECS rule in Java generics?
What is type erasure and what are its practical implications?
Why can't you create an array of a generic type (new T[])?
What is the difference between List<?>, List<Object>, and List<T>?
How does TypeReference in Jackson work around type erasure?
Ask Aria about Advanced Generics
Your personal AI tutor — ask anything about this concept