Generics — Type Parameters, Wildcards, Bounded Types, Type Erasure, and PECS
In this tutorial, you will learn about Generics. We cover key concepts, practical examples, and best practices to help you master this topic.
Java generics enable type-safe programming by parameterizing types, eliminating casts and enabling compile-time Type Checking. Before generics (Java 5), collections held Object references — every retrieval required a cast, and incorrect casts threw ClassCastException at runtime.
What You'll Learn
- Generic classes, methods, and interfaces
- Type parameter naming conventions
- Wildcards:
? extends Tand? super T - Type erasure and its implications
- The PECS principle (Producer Extends, Consumer Super)
Why It Matters
Generics are the backbone of the Collections Framework. Without understanding bounded wildcards, you will struggle to write flexible, reusable code. Type erasure explains weird limitations like "no generic arrays" and "cannot use instanceof with parameterized types."
Real-World Use
List<String>, Map<String, User>, Comparator<Employee> — every collection uses generics. Spring's RestTemplate, JPA's TypedQuery, and every functional interface (Predicate<T>) rely on generics.
Generic Classes
public class Box<T> {
private T value;
public Box(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
Box<String> stringBox = new Box<>("Hello");
String value = stringBox.getValue(); // no cast needed
Multiple Type Parameters
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
Pair<Integer, String> pair = new Pair<>(1, "one");
Generic Methods
public class Utils {
public static <T> T firstElement(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
}
String first = Utils.firstElement(List.of("a", "b"));
Integer max = Utils.max(5, 10);
The type parameter <T> is declared before the return type.
Bounded Type Parameters
Upper Bound (extends)
public static <T extends Number> double sum(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
sum(5, 10); // int -> Integer, OK
sum(3.14, 2.5); // double -> Double, OK
sum("a", "b"); // COMPILE ERROR: String does not extend Number
Multiple Bounds
public static <T extends Comparable<T> & Serializable> void process(T item) {
// T must implement both Comparable and Serializable
}
Wildcards
Unbounded Wildcard (?)
public static void printList(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
Upper Bounded Wildcard (? extends T)
public static double sumOfList(List<? extends Number> list) {
double sum = 0;
for (Number n : list) {
sum += n.doubleValue();
}
return sum;
}
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);
sumOfList(ints); // OK
sumOfList(doubles); // OK
Lower Bounded Wildcard (? super T)
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addNumbers(numbers); // OK
addNumbers(objects); // OK
List<Integer> ints = new ArrayList<>();
addNumbers(ints); // OK
The PECS Principle
PECS stands for Producer Extends, Consumer Super.
- Use
? extends Twhen you produce (read) items of type T - Use
? super Twhen you consume (write) items of type T
// Producer — read only
void copy(List<? extends T> source, List<? super T> dest) {
for (T item : source) { // safe to read
dest.add(item); // safe to write
}
}
Why Not Just Use List<T>?
void copy(List<T> source, List<T> dest) { }
List<Integer> ints = new ArrayList<>();
List<Number> nums = new ArrayList<>();
copy(ints, nums); // COMPILE ERROR: List<Integer> != List<Number>
With wildcards:
void copy(List<? extends T> source, List<? super T> dest) { }
copy(ints, nums); // OK: Integer extends Number, Number super Number
Type Erasure
Generics are a compile-time feature. The compiler erases type parameters and inserts casts:
// Source
Box<String> box = new Box<>("hello");
String s = box.getValue();
// After erasure (roughly)
Box box = new Box("hello");
String s = (String) box.getValue();
Consequences of Erasure
Cannot create arrays of parameterized types:
List<String>[] = new List<String>[10]; // COMPILE ERROR List<String>[] = new List[10]; // OK (unchecked warning)
Cannot use
instanceofwith parameterized types:if (obj instanceof List<String>) { } // COMPILE ERROR if (obj instanceof List) { } // OK (unchecked warning)
Cannot use primitive types as type parameters:
List<int> // COMPILE ERROR — use List<Integer>
Cannot overload methods with same erasure:
void process(List<String> list) { } void process(List<Integer> list) { } // COMPILE ERROR: same erasure
Common Mistakes
- Using raw types.
List list = new ArrayList();bypasses generic type checking. Always use parameterized types. - Mixing generic arrays.
T[] array = new T[10];does not compile due to erasure. UseArrayList<T>instead. - Forgetting wildcards in method parameters.
void Process(List<Number> list)cannot acceptList<Integer>. - Using
? extends Tfor both reading and writing. You cannot add to aList<? extends Number>(except null) because the exact type is unknown. - Assuming
List<Object>is the supertype of allList<T>. It is not.List<String>is not a subtype ofList<Object>.
Practice Questions
1. What is type erasure?
The removal of generic type information at compile time. The compiler replaces type parameters with their bounds or Object, and inserts necessary casts.
2. What is the difference between List<T> and List<?>?
List<T> is a specific parameterized type. List<?> is a list of an unknown type — you can read from it (as Object) but cannot add to it (except null).
3. What does the PECS principle state?
Producer Extends (use ? extends T when reading), Consumer Super (use ? super T when writing).
4. Can you create an array of List<String>?
No. The compiler prevents it because the array would not be type-safe after erasure. Use ArrayList<List<String>> instead.
5. Why does List<Integer> not extend List<Object>?
Because generics are invariant. If List<Integer> were a subtype of List<Object>, you could add a String to what is supposed to be a list of integers.
Challenge Question:
Implement a type-safe copy() method that copies from a source list to a destination list using PECS. Then implement a fill() method that fills a list with values from a supplier. Also implement a flatten() method that takes a List<List<? extends T>> and returns a List<T>.
FAQ
{{< faq "What is @SuppressWarnings(\"unchecked\")?" "An annotation that suppresses unchecked warnings, typically necessary when writing generic code that the compiler cannot fully verify. Use it judiciously and document why the cast is safe." >}}
Mini Project
Write a program GenericsDemo.java that:
- Defines a generic
Repository<T, ID>interface with methodssave(T entity),findById(ID id),findAll(),delete(T entity) - Implements it with an
InMemoryRepository<T, ID>using aMap<ID, T> - Uses bounded type parameters to ensure
IDisSerializable - Creates a
UserRepository extends InMemoryRepository<User, Long> - Demonstrates PECS with a method
void transfer(List<? extends T> source, List<? super T> dest) - Shows raw type warning and how to fix it
- Demonstrates that
List<Integer>is notList<Object>with a failed compilation example
What's Next
Generics make collections type-safe, but equals() and hashCode() make them work correctly. Lesson 28 covers the equals and hashCode contract — why you must override both, how to implement them correctly, and how Lombok and records handle them automatically.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro