What you'll learn
Generics let you write code that works with any type while staying completely type-safe. They're the reason List<String> knows it holds strings — and why you almost never need a cast in modern Java. This module demystifies those angle brackets.
By the end you'll be able to:
- Explain why generics exist and the problems they solve
- Write generic classes, methods, and interfaces
- Constrain type parameters with bounded types
- Use wildcards and apply the PECS principle
- Understand type erasure and its limitations
Why generics exist
Before generics, collections held Object, so you cast every value on the way out and hoped you'd put the right thing in. Mistakes surfaced as ClassCastException at runtime. Generics move that safety to compile time — the compiler enforces the type for you:
Without generics
List list = new ArrayList();
list.add("hi");
String s = (String) list.get(0); // manual cast
list.add(42); // oops, no error...
String bad = (String) list.get(1); // 💥 ClassCastExceptionCasts everywhere, and mistakes blow up at runtime.
With generics
List<String> list = new ArrayList<>();
list.add("hi");
String s = list.get(0); // no cast!
list.add(42); // ✗ compile errorType-safe, cast-free, and errors caught at compile time.
Generic classes
A generic class takes a type parameter in angle brackets — conventionally <T> for "type," <E> for "element," <K, V> for "key, value." Inside the class, T stands in for whatever type the caller chooses when they create an instance:
class Box<T> { // T is a type parameter
private T item;
void set(T item) { this.item = item; }
T get() { return item; }
}
public class Main {
public static void main(String[] args) {
Box<String> b = new Box<>();
b.set("Hello");
String s = b.get(); // no cast needed
System.out.println(s);
}
}Note
<> on the right (new Box<>()) is the diamond operator — the compiler infers the type from the left side, so you don't repeat it.Generic methods
A single method can be generic too, even in a non-generic class. Declare the type parameter before the return type. The compiler infers T from the arguments you pass, so one method works for every type:
// <T> before the return type declares a generic method
static <T> T firstOf(T[] array) {
return array[0];
}
public static void main(String[] args) {
String[] names = {"Sara", "Omar"};
Integer[] nums = {1, 2, 3};
System.out.println(firstOf(names)); // Sara
System.out.println(firstOf(nums)); // 1
}Bounded type parameters
Sometimes T can't be anything — you need it to have certain capabilities. A bounded type parameter (<T extends Number>) restricts T to a type or its subtypes, letting you call that type's methods inside:
import java.util.List;
// T must be a Number (or subclass), so we can call number methods
static <T extends Number> double sum(List<T> list) {
double total = 0;
for (T n : list) total += n.doubleValue();
return total;
}
public static void main(String[] args) {
System.out.println(sum(List.of(1, 2, 3))); // 6.0
System.out.println(sum(List.of(1.5, 2.5))); // 4.0
}Wildcards & the PECS principle
The wildcard ? represents an unknown type — useful in method parameters when you want flexibility. It comes in three forms:
?
Unbounded
Any type. Read elements as Object. Use when the type doesn't matter.
? extends T
Upper bound
T or any subtype. A producer — you read T out of it.
? super T
Lower bound
T or any supertype. A consumer — you write T into it.
import java.util.List;
// accepts a List of ANY type
static void printAll(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
public static void main(String[] args) {
printAll(List.of(1, 2, 3));
printAll(List.of("a", "b"));
}PECS: Producer Extends, Consumer Super
? extends T when a structure produces values you'll read, and ? super T when it consumes values you'll write. It tells you exactly which wildcard to reach for.Type erasure
Generics are a compile-time feature. After the compiler checks your types, it erases them — at runtime a List<String> is just a List. This keeps generics compatible with old code, but it has consequences:
- You can't write
new T()ornew T[]— the type isn't known at runtime - You can't use
instanceof List<String>— onlyinstanceof List - Two overloads that differ only by generic type (
List<String>vsList<Integer>) clash
Note
Recap & quick check
Key takeaways
- Generics provide compile-time type safety and remove the need for casts.
- Generic classes and methods use a type parameter like <T> that the caller fills in.
- Bounded types (<T extends Number>) let you require capabilities on T.
- Wildcards: ? (any), ? extends T (producer/read), ? super T (consumer/write) — remember PECS.
- Type erasure removes generic info at runtime, which is why new T() and generic instanceof aren't allowed.
Quick check
1. What is the main benefit of generics?
2. In List<String>, what is String?
3. What does <T extends Number> mean?
4. PECS stands for…
5. Because of type erasure, what is NOT allowed?
Nicely done — generics unlock type-safe, reusable code. Next up: Module 14 — the Java Collections Framework, the most-used generic library of all.