Career OS

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 you’ll learn
  • 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
✅ After this you’ll be able to
  • 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

✕ Without generics

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.

✓ With generics

List<String> — the compiler catches a wrong type before you run, and no casting needed. Bugs found early are cheap.

Reading them

Where you’ll use it — real life

📦 Every collection

You already use generics whenever you type List<Order>.

🛡️ Fewer bugs

Type errors caught at compile time never reach production.

♻️ Reusable code

Write one method that works for any type, safely.

📖 Readability

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.String

The 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 knows

The 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.

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 Integer

You 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

Erasure is why generics feel like a compiler feature rather than a JVM feature — because that’s exactly what they are.

🔨 Build this — 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.

🧠 Interview check — can you answer these?
  • Where does a generic move a type error — and from what to what? (runtime crash → compile-time red line)
  • In Box<T>, who decides what T is, and when? (the user, at new Box<String>() — each instance can differ)
  • What does <T extends Comparable<T>> buy you inside the method? (the right to call compareTo on T)
  • Difference between a generic class and a generic method? (class carries T on every instance; method’s T lives 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 runtime T is Object, no real class to build)
🗣️ The 2-minute explain test

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 →

Saves your progress on this device.
00:00