Learn · OOP & Java
Generics
Generics let you write code that works with any type while still catching type mistakes before the program even runs. Those <angle brackets> you see everywhere — this is them.
Before we start
- What generics are and the problem they solve
- How List<String> gives you type safety
- Why generics catch bugs at compile time
- Reading angle-bracket types
- Explain why List<String> beats a raw list
- Read and write basic generic types
- Say what type safety buys you
Why you’re learning it: every collection is generic (List<String>), and understanding them makes Java code readable instead of mysterious. ⏱️ ~20 min.
The idea — a labelled box
Imagine a storage box. A plain box could hold anything — but then you never know what you’ll pull out, and you might grab an apple expecting a book. A labelled box — “Apples only” — guarantees what’s inside. List<String> is a box labelled “Strings only”: the compiler won’t let you put a number in, and won’t make you double-check what you take out.
What it saves you
A raw list holds anything. You must cast on the way out, and a wrong type explodes at runtime — the worst time to find out.
List<String> — the compiler catches a wrong type before you run, and no casting needed. Bugs found early are cheap.
Reading them
List<String>— a list of StringsMap<String, Integer>— a map from String keys to Integer values<T>— a placeholder “some type”, filled in when used (how the library authors write reusable code)
Where you’ll use it — real life
You already use generics whenever you type List<Order>.
Type errors caught at compile time never reach production.
Write one method that works for any type, safely.
The type tells the next dev exactly what’s inside.
See the crash for yourself
Before Java 5, lists just held Object — anything could go in, and you had to cast on the way out. Here’s that old world, and the trap it sets:
List names = new ArrayList(); // raw list — holds Object, so ANYTHING goes in
names.add("Darshan");
names.add(42); // compiler shrugs. It shouldn't.
for (Object o : names) {
String name = (String) o; // you PROMISE the compiler this is a String
System.out.println(name.toUpperCase());
}This compiles with only a warning, then explodes when it runs:
DARSHAN
Exception in thread "main" java.lang.ClassCastException:
class java.lang.Integer cannot be cast to class java.lang.StringThe bug was on the names.add(42) line — but the crash happens later, somewhere else, maybe months later in production. That gap between “bug here” and “crash there” is what makes runtime errors expensive. Now the generic version:
List<String> names = new ArrayList<>();
names.add("Darshan");
names.add(42); // COMPILE ERROR: incompatible types — caught before it ever runs
String name = names.get(0); // no (String) cast needed — the compiler already knowsThe wrong line is now the line that errors. You fix it in ten seconds, in your editor. That’s the whole job of generics: move the explosion from runtime to compile time — and the cast disappears as a bonus.
Read any type out loud
Every javadoc, every library method, every Spring interface is written in angle-bracket language. If you can say a type as a sentence, you understand it. When one looks scary, read it inside-out — innermost type first.
List<String>— “a list of strings”Map<String, Integer>— “a map from string to integer”Map<String, List<Expense>>— “a map from string to a list of expenses”Optional<User>— “maybe a user”JpaRepository<User, Long>— “a repository of users, whose id type is long”
Writing your own — the <T> placeholder
A generic class has a type parameter — a placeholder the user fills in. Convention: T for type, E for element, K/V for key/value. Here’s the labelled box, in code:
public class Box<T> {
private T value; // T is whatever the user said it is
public void put(T value) { this.value = value; }
public T get() { return value; } // no cast — the compiler tracks T
}Box<String> nameBox = new Box<>(); // T = String, for this box only
nameBox.put("Darshan");
String name = nameBox.get(); // compiler KNOWS this is a String
Box<Integer> ageBox = new Box<>(); // same class, T = Integer here
ageBox.put(24);One class, infinite type-safe variations. Two parameters works the same way — and this exact shape is Spring’s JpaRepository<User, Long>: an entity type and an id type.
public class Pair<A, B> {
private final A first;
private final B second;
public Pair(A first, B second) { this.first = first; this.second = second; }
public A first() { return first; }
public B second() { return second; }
}
Pair<String, Integer> upiTxn = new Pair<>("chai stall", 20);Generic methods
You don’t need a generic class for one generic method. Declare the placeholder in angle brackets before the return type:
// the <T> before T declares T for this method only
public static <T> T firstOrNull(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
String first = firstOrNull(List.of("a", "b")); // T inferred as String
Integer num = firstOrNull(List.of(1, 2, 3)); // T inferred as IntegerYou almost never spell out T at the call site — the compiler infers it from the arguments.
Bounded types — when “any type” is too loose
Want a max() for any list? With plain T, the compiler only lets you call Object methods on it — it has no idea T can be compared. A bound fixes that:
// read aloud: "any T that knows how to compare itself to other Ts"
public static <T extends Comparable<T>> T max(List<T> items) {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) best = item; // legal ONLY because of the bound
}
return best;
}
max(List.of(45, 12, 99, 7)); // 99
max(List.of("mango", "apple", "zen")); // "zen"<T extends Comparable<T>> is a deal: “I’ll accept any type, as long as it implements Comparable.” In return, the compiler lets you call compareTo inside. Passing a plain Object won’t compile — it never signed the deal.
Wildcards — recognize, don’t memorize
You’ll see ? in library signatures constantly. For now, just learn to read them — skip the PECS rule until later:
List<? extends Number>“a list of Number or any subtype” — safe to read Numbers out, not safe to add.
List<? super Integer>“a list of Integer or any supertype” — safe to add Integers in.
Real JDK example: boolean addAll(Collection<? extends E> c) — “give me a collection of E or anything more specific.” When a wildcard confuses you in the wild, translate it to one of those two sentences and move on.
Type erasure in 3 lines
- Generics exist only at compile time — the compiler checks everything, then erases
TtoObjectin the bytecode. - At runtime, a
List<String>and aList<Integer>are the same class: justList. - That’s why
new T()andnew T[10]are illegal — at runtimeTdoesn’t exist to construct.
Erasure is why generics feel like a compiler feature rather than a JVM feature — because that’s exactly what they are.
InMemoryRepository<T>A baby version of what Spring Data hands you for free later: one class, reused for two completely different types.
public class InMemoryRepository<T> {
private final List<T> items = new ArrayList<>();
public T save(T item) { items.add(item); return item; }
public List<T> findAll() { return new ArrayList<>(items); } // a copy
public long count() { return items.size(); }
}record Expense(String description, int amountInRupees) {}
record Friend(String name, String city) {}
InMemoryRepository<Expense> expenses = new InMemoryRepository<>();
expenses.save(new Expense("chai", 20));
InMemoryRepository<Friend> friends = new InMemoryRepository<>();
friends.save(new Friend("Arjun", "Bengaluru"));Then break it on purpose: add expenses.save(new Friend("Ravi", "Pune")); and recompile. That red line is generics doing its one job. Delete it after.
- Where does a generic move a type error — and from what to what? (runtime crash → compile-time red line)
- In
Box<T>, who decides whatTis, and when? (the user, atnew Box<String>()— each instance can differ) - What does
<T extends Comparable<T>>buy you inside the method? (the right to callcompareToonT) - Difference between a generic class and a generic method? (class carries
Ton every instance; method’sTlives only for that one call) List<? extends Number>— what’s safe to do? (read out as Number; can’t add, except null)- Why can’t you write
new T()? (erasure — at runtimeTisObject, no real class to build)
Out loud: “What problem do generics solve, and why is catching a type error at compile time better than at runtime?” Log it in your Journal.
Back to your Siemens roadmap →