Generics & type safety.
Every Spring repository, every JPA entity, every modern Java method signature uses generics. Without understanding them, modern code is just noise. With them, the compiler becomes your strongest debugging tool.
The pre-2004 pain — why generics exist
Before Java 5, collections held Object — the universal supertype. So you could put anything in:
List list = new ArrayList();
list.add("hello");
list.add(42); // totally legal
list.add(new User()); // also legal!
String s = (String) list.get(1); // 💥 ClassCastException at runtimeThree problems:
- You couldn't tell from a method signature what kind of objects a list held
- You had to cast on every retrieval
- Type errors crashed at runtime, not at compile time
Move type errors from runtime (in production, at 3am) to compile time (in your IDE, while you're writing). That's it. That's the whole point.
What <T> actually does
The angle brackets declare a type parameter — a placeholder that gets filled in when the class is used.
// A generic class — T is a placeholder
public class Box<T> {
private T content;
public void set(T value) { this.content = value; }
public T get() { return content; }
}
// Usage — fill in the T
Box<String> stringBox = new Box<>();
stringBox.set("hi");
String s = stringBox.get(); // no cast needed
stringBox.set(42); // 💥 won't compile — wrong typeThe compiler enforces: this Box can only hold Strings. Try to put an int → compiler error before your code even runs.
The trick — type erasure
Here's the part that surprises everyone: generics exist only at compile time. After compilation, the type parameters are erased. The bytecode treats Box<String> the same as Box<Integer> — both are just Box.
What you write
List<String> names = new ArrayList<>();
names.add("alice");
String name = names.get(0);What the JVM sees
List names = new ArrayList();
names.add("alice");
String name = (String) names.get(0); // cast injectedWhy care? Because erasure has consequences:
Consequence 1 · You can't do this
if (obj instanceof List<String>) { ... } // 💥 won't compileAt runtime, there's no type info. You can only check raw List.
Consequence 2 · You can't create generic arrays
T[] arr = new T[10]; // 💥 won't compileWorkaround: use List<T> instead, or Array.newInstance() with a class token.
Consequence 3 · You can't have static fields of type T
class Box<T> {
static T cache; // 💥 won't compile
}Static is one-per-class. T is per-instance. Conceptually they fight.
Java had millions of lines of pre-generic code in production. Sun chose erasure to keep backward compatibility — generic code can call non-generic code and vice versa. C# took the opposite road (reified generics), which is more powerful but broke compatibility. Java's choice: pragmatism over purity.
Wildcards — ?, extends, super
This is where most learners give up. Don't. There are only three patterns and one mnemonic.
The unbounded wildcard <?>
"A list of some type, I don't know which." You can read from it (as Object) but can't add to it.
void printAll(List<?> items) {
for (Object o : items) System.out.println(o);
// items.add(...) — won't compile, type unknown
}Upper bounded · <? extends T> · "PRODUCER"
"Some subtype of T (or T itself)." You can read safely as T. You cannot add (you don't know the exact subtype).
double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) total += n.doubleValue();
return total;
}
// All of these now work:
sum(List.of(1, 2, 3)); // List<Integer>
sum(List.of(1.0, 2.5)); // List<Double>Lower bounded · <? super T> · "CONSUMER"
"Some supertype of T (or T itself)." You can add Ts to it safely. You cannot read as T (only as Object).
void addNumbers(List<? super Integer> bag) {
bag.add(1);
bag.add(42); // safe — Integer fits anywhere accepting Integer or higher
}
// All of these work:
addNumbers(new ArrayList<Integer>());
addNumbers(new ArrayList<Number>());
addNumbers(new ArrayList<Object>());Producer Extends, Consumer Super.
If you'll read from it (it produces things) → <? extends T>
If you'll write to it (it consumes things) → <? super T>
This is from Effective Java, item 31. Top-3 most-asked Java interview question.
Bounded type parameters
Sometimes you want to require T to have certain capabilities. Use <T extends ...>:
// T must implement Comparable so we can call .compareTo()
public <T extends Comparable<T>> T max(List<T> list) {
T best = list.get(0);
for (T item : list) {
if (item.compareTo(best) > 0) best = item;
}
return best;
}How Spring uses generics — recognize this
You'll see these patterns constantly:
// Spring Data JPA — define your repo interface
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
// JpaRepository<T, ID> — T is your entity, ID is its primary-key type
// Spring generates the implementation at runtime using these types
// ResponseEntity — generic over the response body type
ResponseEntity<UserDto> response = ResponseEntity.ok(userDto);
// Optional — wraps "value or absence"
Optional<User> maybe = userRepo.findById(id);When you see JpaRepository<User, Long>, you're not reading magic. You're reading: "I'm a repository for the User entity, whose ID is of type Long." The framework uses these type parameters via reflection to know what table to query and what type to expect back.
Generic methods — the angle brackets in front
You can declare type parameters on a single method, even in a non-generic class:
public <T> List<T> repeat(T item, int n) {
List<T> out = new ArrayList<>();
for (int i = 0; i < n; i++) out.add(item);
return out;
}
// Usage — T is inferred
List<String> hellos = repeat("hi", 3);
List<Integer> zeros = repeat(0, 5);Common generic mistakes
- Using raw types in new code.
List list = ...instead ofList<String>. The compiler warns; ignore at your peril. - Excessive wildcards. If your method takes
List<? extends Object>, that's justList<?>. Don't be fancy. - Trying to use T at runtime. Erasure bites — pass
Class<T>as a parameter when you need it. - Confusing extends in generics with extends in inheritance. They use the same keyword but mean different things.
Lock in today's learning
If any answer is unclear, that's the spot to re-read.
- What single problem did Java 5 generics solve? Express it in one sentence.
- What is type erasure? What's one thing it prevents you from doing?
- State PECS and give a 1-line code example for each side.
- Why does
List<Integer>not work whereList<Number>is expected? (Hint: invariance) - How does Spring Data use generics in
JpaRepository<T, ID>? - What's the difference between
<T extends Number>in a class declaration vs.<? extends Number>in a method parameter?
End of Day 5. Tomorrow: Java 8 reading fluency — Streams, Lambdas, Optional. The features you'll see in every modern Spring codebase.