Why Generics
- Type safety at compile time (less
ClassCastException). - Code reuse for different types.
- Self-documenting API:
Map<String, Integer>is clearer thanMap.
Basic Syntax: <T>, <E>, <K, V>
Class/Interface with Type Parameter
public class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
public void set(T value) { this.value = value; }
}
Box<String> sb = new Box<>("hi");
String s = sb.get(); // safe, no castsMultiple Parameters
public class Pair<K, V> {
private final K key; private final V value;
public Pair(K key, V value) { this.key = key; this.value = value; }
public K key() { return key; } public V value() { return value; }
}Generic Methods
public static <T> T first(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}Bounds
public class NumberBox<T extends Number> { /* ... */ }
// several bounds:
public static <T extends Number & Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}Wildcards: ?, ? extends, ? super + PECS principle
Invariance: The key to understanding
List<Integer> is not a List<Number>, nor a subtype, nor a supertype.
Arrays are covariant (Number[] arr = new Integer[10]), generics are invariant.
Wildcard ?
List<?> is a list of unknown type: you can read it as Object, but you cannot add anything (except null).
Upper Bound: ? extends T (“producer”)
void printNums(List<? extends Number> src) {
// can read as Number
Number n = src.get(0);
// src.add(42); // cannot - type is unknown
}Lower Bound: ? super T (“consumer”)
void addIntegers(List<? super Integer> dst) {
dst.add(1); // can add Integer (and its subtypes)
// Integer x = dst.get(0); // returns Object, unsafe to read
}The Golden Rule PECS
- Producer Extends: if you read (source), use
? extends. - Consumer Super: if you write (consumer), use
? super.
Classic Example: Collections.copy
public static <T> void copy(List<? super T> dest, List<? extends T> src) { ... }Source – extends, consumer – super.
Frequently Asked Questions and Pitfalls
Why List<Object> ≠ List<String>?
Invariance: otherwise, you could insert an Integer into List<String> via a reference to List<Object>.
Use List<?> if you need “any list for read-only”.
What is the difference between List<? extends Number> and List<Number>?
- In
List<Number>, you can addInteger,Long,Double, etc. - In
List<? extends Number>, you cannot add (the specific type is unknown), but you can safely read asNumber.
What is List<? super Integer> useful for?
It’s a “bucket” for writing Integer objects. You can only read from it as Object.
Wildcard Capture
Sometimes you need a “helper” to work with ?:
static void swapFirst(List<?> list) { swapHelper(list); }
private static <T> void swapHelper(List<T> list) {
if (list.size() > 1) {
T tmp = list.get(0);
list.set(0, list.get(1));
list.set(1, tmp);
}
}Type Inference
“Diamond” (diamond operator) with Java 7+
Map<String, List<Integer>> m = new HashMap<>();Inference in Generic Methods
var x = List.of(1, 2, 3); // List<Integer>
var y = first(List.of("a")); // StringSometimes explicitly specify <T> in the call: MyClass.<Long>first(...).
Type Erasure
What it is: Type parameters exist only at compile time.
JVM sees “raw” types: List<T> → List. From here the restrictions:
Consequences of Erasure
- Cannot create an array of parameterized type:
List<String>[] arr = new List<String>[10]; // error- Cannot create
new T()andT.class:
T t = new T(); // error
Class<T> c = T.class; // errorWorkaround: pass Class<T> to the constructor/method:
class Factory<T> {
private final Class<T> type;
Factory(Class<T> type) { this.type = type; }
T create() throws Exception { return type.getDeclaredConstructor().newInstance(); }
}- Cannot overload methods differing only in type parameters:
void foo(List<String> x) {}
void foo(List<Integer> y) {} // same erasure → conflictinstanceofonly with wildcards or raw types:
if (obj instanceof List<?>) { /* ok */ }
// if (obj instanceof List<String>) // compile error- Bridge methods: The compiler generates them to preserve polymorphism during erasure (often visible in bytecode/debug).
- Reifiable vs non-reifiable:
- Reifiable (preserve info at runtime):
List<?>,List, primitive arrays. - Non-reifiable:
List<String>,Map<Integer, String>– the type parameter is erased.
Type token / preserving type
They use a “type holder”:
abstract class TypeRef<T> {
final Type type = ((ParameterizedType) getClass()
getGenericSuperclass()).getActualTypeArguments()[0];
}
TypeRef<List<String>> ref = new TypeRef<>() {};
System.out.println(ref.type); // java.util.List<java.lang.String>Generics and Arrays/Varargs
- Arrays are covariant and checked at runtime; generics are invariant and checked at compile time.
- Varargs with generics lead to a
heap pollutionwarning.
Mark the method@SafeVarargs(onlystatic/final/private) and don’t mix internals:
@SafeVarargs
public static <T> List<T> asList(T... items) { return Arrays.asList(items); }Practical Signatures for Interviews
Maximum on Comparator
public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
if (list.isEmpty()) return null;
T best = list.get(0);
for (T t : list) if (t.compareTo(best) > 0) best = t;
return best;
}Copying from source to consumer (PECS)
public static <T> void copyAll(List<? super T> dest, List<? extends T> src) {
for (T t : src) dest.add(t);
}Generalized repository in Spring style
public interface Repository<T extends BaseEntity, ID> {
T save(T entity);
Optional<T> findById(ID id);
}Typical Errors
- ❌ Raw types (
List list) → lose safety,ClassCastExceptionarises. - ❌ Returning
List<? extends T>from an API method. It’s better to returnList<T>; wildcards are in arguments, not return types. - ❌ Overusing
? extendswhere you should write. Remember PECS. - ❌ Trying to overload methods differing only in type parameters (erasure will break).
- ❌
List<Object>instead ofList<?>for “any read-only collection”.
Best practices
- Follow PECS:
extends– read,super– write. - In public APIs use wildcards in parameters, not in return types.
- Avoid raw types; if you have to, isolate and accompany
@SuppressWarnings("unchecked")with a comment. - Give meaningful names to type parameters:
T,E,K,V,ID,R,U. - For factories/reflection, take
Class<T>orType(type token), don’t inventnew T().
Mini-cheat sheet of differences (for memory)
List<?>– read asObject, don’t write.List<? extends T>– read asT, don’t write.List<? super T>– writeT, read asObject.- Generics are invariant; arrays are covariant.
- Type erasure: no
new T(),instanceof List<String>and overloads “only by generics”.