Generics

Before Java 5, a list could hold anything, so every read needed a cast, and a wrong element crashed the program only when someone finally read it. Generics let you write List<String>: the compiler now knows what's inside, rejects wrong elements immediately, and inserts the casts for you. You'll learn to use generic types, write your own generic classes and methods, control them with bounds and wildcards, and see what's left of them at run time.

Module 11 · Collections⏱ 24 minIntermediate8 practice problems
1 The story

The integer that sneaked into the name list

An old attendance module stores names in a plain ArrayList with no type. Someone's import script adds a roll number, 21007, by mistake. Nothing complains. Two weeks later the monthly report crashes with ClassCastException, in a file nobody touched.

List names (raw): anything goes names.add("Riya"); names.add(21007);← no error two weeks later, another file: String s = (String) names.get(1); ClassCastException at run time List<String> names: only Strings names.add("Riya"); names.add(21007); ✗ javac: int cannot be converted to String String s = names.get(0);no cast myphatshala.in
Riya

Sir, the crash is in the report code, but the bug was in the import script two weeks ago. How do I even find that?

Arjun Sir

That's the problem generics solve. Write List<String> and the compiler knows the list holds only Strings. The wrong add(21007) becomes a compile error, right where the mistake is made.

Riya

And I don't have to cast when I read?

Arjun Sir

No casts. The compiler checks everything going in, so it can safely insert the cast when things come out. Then, a surprise: once the checking is done, javac erases most of the type information. Let's see why that's fine.

2 Definition

What are generics?

Formal definition (write this in exams)

Generics (Java 5) allow classes, interfaces and methods to be declared with type parameters, such as class Box<T> or <T> T max(List<T> xs), which are replaced by actual type arguments when used (Box<String>). They provide compile-time type safety and remove the need for explicit casts. Type parameters can be restricted with bounds (T extends Number), and wildcards (?, ? extends T, ? super T) express flexible parameter types. Generics are implemented by type erasure: the compiler checks the types, then removes them, replacing type parameters with their bounds (usually Object) and inserting casts.

In simple words

A label on a container saying what it holds: List<String> is “a list of Strings”. The compiler enforces the label, so wrong items are caught while you write code, not when users run it.

Words you'll meet in this lesson

Type parameterThe placeholder in a declaration: T in class Box<T>.
Type argumentThe real type you supply: String in Box<String>.
Diamond operatorThe empty brackets in new ArrayList<>(): javac infers the type argument (Java 7+).
BoundA limit on T: T extends Comparable<T> means T must be comparable.
Wildcard? = some unknown type. List<? extends Number> = a list of some kind of Number.
Type erasureGeneric types are checked at compile time and removed from the bytecode.
Raw typeA generic type used without arguments (List): legacy, unsafe, gives warnings.
Naming conventionT type, E element, K/V key/value, R result
Type argumentsReference types only: List<Integer>, never List<int>
CheckedAt compile time; at run time List<String> and List<Integer> are the same class
Not allowednew T(), new T[10], instanceof List<String>, static fields of type T

Exam tip: generics are invariant: List<String> is not a subtype of List<Object>, even though String is a subtype of Object (incompatible types: ArrayList<String> cannot be converted to List<Object>). Use a wildcard, List<?> or List<? extends Object>, when a method must accept lists of any element type.

3 Types and ways to use it

Four ways generics appear in code

1. Using generic types

List<String>, Map<String, Integer>: the everyday case.

collections
T

2. Generic classes

class Box<T>: write once, use with any type.

containers, results

3. Generic methods

<T> T first(List<T> xs): type decided per call.

utilities

4. Bounds and wildcards

T extends Number, ? extends, ? super: flexible but safe.

APIs

Your own generic class

class Box<T> {
    private T item;

    void put(T item) { this.item = item; }
    T get() { return item; }
}

public class Main {
    public static void main(String[] args) {
        Box<String> parcel = new Box<>();
        parcel.put("Laptop");
        String what = parcel.get();             // no cast needed
        System.out.println(what.toUpperCase());

        Box<Integer> locker = new Box<>();
        locker.put(42);
        int n = locker.get() + 1;               // unboxed automatically
        System.out.println(n);
        // parcel.put(99);                      // compile error: int cannot be converted to String
    }
}
LAPTOP 43

Generic methods and bounded types

import java.util.List;

public class Main {
    // T must be comparable to itself, so compareTo is allowed
    static <T extends Comparable<T>> T max(List<T> xs) {
        T best = xs.get(0);
        for (T x : xs) {
            if (x.compareTo(best) > 0) best = x;
        }
        return best;
    }

    public static void main(String[] args) {
        System.out.println(max(List.of(3, 9, 4)));            // T = Integer
        System.out.println(max(List.of("kiwi", "apple")));    // T = String
    }
}
9 kiwi

Wildcards: ? extends to read, ? super to write

import java.util.ArrayList;
import java.util.List;

public class Main {
    static double sum(List<? extends Number> xs) {      // any list of numbers: read only
        double s = 0;
        for (Number n : xs) s += n.doubleValue();
        return s;
    }

    static void addDefaults(List<? super Integer> xs) {  // any list that can hold Integers: write
        xs.add(1);
        xs.add(2);
    }

    public static void main(String[] args) {
        System.out.println(sum(List.of(1, 2.5, 3L)));      // Integer, Double, Long together
        List<Number> nums = new ArrayList<>();
        addDefaults(nums);
        System.out.println(nums);
    }
}
6.5 [1, 2]

Remember PECS: Producer Extends, Consumer Super. If a parameter produces values you read, use ? extends T. If it consumes values you add, use ? super T.

Type erasure, proved with javap

List<String> s = new ArrayList<>();
s.add("x");
String t = s.get(0);
invokeinterface  java/util/List.add:(Ljava/lang/Object;)Z     ← add(Object): no String here
invokeinterface  java/util/List.get:(I)Ljava/lang/Object;    ← get returns Object
checkcast        java/lang/String                            ← the cast javac inserted for you

Raw types defeat all of this. List raw = new ArrayList(); compiles with an unchecked warning and accepts anything; assigning it to a List<String> lets a wrong element slip through, and the ClassCastException appears far away, at the hidden cast. Never use raw types in new code.

Rule of thumb: use the diamond on the right (new HashMap<>()), the interface with full type arguments on the left, and treat every unchecked warning as a bug report.

4 Step by step

What javac does with List<String>

  1. Check every call against the type argument

    List<String> names = new ArrayList<>();
    names.add(21007);   // checked: int → String? No.
    javac substitutes E = String into add(E e) and reports incompatible types: int cannot be converted to String. No class file is written.
  2. Infer the diamond

    new ArrayList<>() on the right becomes new ArrayList<String>() from the variable's type on the left.
  3. Erase the type arguments

    In the bytecode, List<String> becomes plain List, E becomes Object (or its bound, e.g. Comparable for T extends Comparable<T>).
  4. Insert casts where values come out

    String s = names.get(0) compiles to (String) names.get(0), a checkcast instruction. It can't fail, because step 1 guaranteed only Strings went in.
  5. At run time, one class for all

    new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass() is true. The object on the heap has an Object[] inside, whatever the type argument was. Erasure kept Java 5 code compatible with older libraries.
5 See it happen

Checked, then erased: both views

Left: the code you write with generics. Right: what javac checks, and what remains in the class file.

Generics: checked, then erased 6 steps
What you see List<String> names = new ArrayList<>(); names.add("Riya"); names.add(21007); String s = names.get(0); // javac writes the class file // the program runs javac / console ✗ int cannot be converted to String ✓ compiles (after removing the bad line) RIYA A list labelled: Strings only. A String goes in. A number is refused. Reading needs no cast. Compile. Run. myphatshala.in
Behind the screen interface List<E> { boolean add(E e); E get(int i); } javac check E = String (diamond → ArrayList<String>) add(E) = add(String) ← "Riya" ✓ add(String) ← int 21007 ✗ stop get(int) returns E = String ✓ class file (after erasure) List.add(Object) List.get(int) → Object checkcast String ← inserted heap at run time ArrayList elementData: Object[] {"Riya"} no <String> anywhere E is replaced by String for checking. The argument matches add(String). int doesn't fit add(String). The result type is known: String. Types erased; a cast is added. One ArrayList class for all types. myphatshala.in
6 Real-world use

Generics in real systems

Generic types are everywhere in modern Java: collections, API responses, repositories and caches.

Watch it with an example: one response class for a whole app

A food-delivery backend sends every reply in a generic ApiResponse<T>. Watch the same class carry an order, a list of restaurants and an error.

One generic class, many payloads 4 steps
Animated example class ApiResponse<T> { boolean ok; T data; String error; } ApiResponse<Order> ApiResponse<List<Shop>> ApiResponse<Order> T = ? JSON sent to the app ok: true data: {id: 9912, total: 349} ok: true data: [Chai Point, Dosa Hut] ok: false data: null error: "Restaurant closed" compiler-checked no casts in callers One class, written once. T = Order for the order screen. T = a list of shops for the home screen. Errors use the same shape. myphatshala.in
Response<Order>ok, data, error

1. API response wrappers

Backend teams wrap every reply in one generic class, ApiResponse<T>, with status, data and error fields. ApiResponse<Order> and ApiResponse<List<Product>> share one implementation.

controller→ApiResponse<Order>→JSON to the app
Repository< Customer, Long>

2. Data repositories

Frameworks like Spring Data define CrudRepository<T, ID>; your CustomerRepository extends CrudRepository<Customer, Long> gets type-safe findById, save and delete without writing them.

repo.findById(7L)→Optional<Customer>→no casts anywhere
Cache<K, V>

3. Caches and utilities

A team writes Cache<K, V> once and uses it for user sessions, exchange rates and product images. Utility methods like Collections.max and Comparator.comparing are generic too.

Cache<String, Rate>→get(K) : V→typed result
unchecked

4. Cleaning up legacy code

Old modules full of raw List and Map produce unchecked warnings. Adding type arguments turns hidden run-time crashes into compile errors, one file at a time.

raw List→List<Invoice>→bugs caught by javac

Examples are simplified; framework class names are given as common examples.

7 Run the code

A generic result type for any lookup

import java.util.HashMap;
import java.util.Map;

class Result<T> {
    private final T value;
    private final String error;

    private Result(T value, String error) {
        this.value = value;
        this.error = error;
    }

    static <T> Result<T> ok(T value) { return new Result<>(value, null); }
    static <T> Result<T> fail(String error) { return new Result<>(null, error); }

    boolean isOk() { return error == null; }
    T value() { return value; }
    String error() { return error; }
}

public class Main {
    static Map<String, Integer> stock = new HashMap<>(Map.of("pen", 12, "book", 0));

    static Result<Integer> available(String item) {
        Integer n = stock.get(item);
        if (n == null) return Result.fail("No such item: " + item);
        if (n == 0) return Result.fail(item + " is out of stock");
        return Result.ok(n);
    }

    public static void main(String[] args) {
        for (String item : new String[]{"pen", "book", "lamp"}) {
            Result<Integer> r = available(item);
            System.out.println(r.isOk() ? item + ": " + r.value() + " left" : r.error());
        }
    }
}
pen: 12 left book is out of stock No such item: lamp
  • class Result<T>One class works for any result type: Result<Integer>, Result<Order>…
  • static <T> Result<T> ok(T value)A generic factory method: T is inferred from the argument.
  • Result<Integer> available(...)Callers know exactly what value() returns: an Integer, no cast.
  • Result.fail("…")T is inferred from the method's return type.
8 Line by line

A generic Box at run time

This trace is recorded from the real JVM. The two boxes are declared with different type arguments, but on the heap they're the same class holding plain object references: the type arguments were erased after javac checked them.

Worked examples: real problems, solved and traced in memory

Three reusable pieces of code made type-safe with generics: a bounded generic method, a wildcard method and a two-parameter generic class.

Example 1Sports · bounded generic method

Top score from any comparable list

Problem statement

Write one method that returns the largest element of a list, and use it for runs (Integers) and for player names (Strings, alphabetically last).

Input
[34, 71, 12] and ["Om", "Riya", "Kabir"]
Expected output
71 Riya

Approach

  1. Declare <T extends Comparable<T>> T best(List<T> xs): the bound allows compareTo.
  2. Loop and keep the larger element.
  3. Call it twice: javac infers T = Integer, then T = String.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 2Finance · wildcard

Total of mixed amount lists

Problem statement

Bills come as List<Integer> (whole rupees) and refunds as List<Double>. Write one method that totals either.

Input
bills [499, 250], refunds [99.5]
Expected output
Bills 749.0, refunds 99.5

Approach

  1. Parameter type List<? extends Number>: a producer of Numbers.
  2. Read each element as a Number and add doubleValue().
  3. Call it with both lists.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 3Education · generic class

Pairs of student and grade

Problem statement

A results screen shows pairs like (Riya, A). Write a generic Pair<K, V> and use it for (String, Character) and (String, Integer).

Input
(Riya, 'A'), (Kabir, 78)
Expected output
Riya=A Kabir=78 A 80

Approach

  1. class Pair<K, V> with two final fields and a toString.
  2. Create Pair<String, Character> and Pair<String, Integer>.
  3. Read value back without casts.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

9 Quiz and revision

Check yourself

Quick revision

Lesson 11.2 in one screen

  • Generics = type parameters: class Box<T>, <T> T first(List<T>); use with type arguments: Box<String>.
  • Benefits: compile-time checks, no casts, reusable code. Diamond <> infers arguments.
  • Bounds: T extends Number, T extends Comparable<T>.
  • Wildcards: ? extends T to read (producer), ? super T to write (consumer): PECS.
  • Invariant: List<String> is not a List<Object>.
  • Erasure: types checked then removed; casts inserted; one class at run time.
  • Not allowed: primitives as arguments, new T(), generic arrays, instanceof List<String>. Avoid raw types.

Interview and viva questions

What is type erasure and why did Java choose it?

The compiler enforces generic types, then removes them from the bytecode, replacing type parameters with their bounds and adding casts. It was chosen for backward compatibility: generic code from Java 5 could run on the same JVM and interoperate with pre-generics libraries.

Explain PECS.

Producer Extends, Consumer Super. If a parameter supplies values to your method, declare it ? extends T (you can read T from it). If it receives values from your method, declare it ? super T (you can add T to it). Collections.copy(List<? super T> dest, List<? extends T> src) is the classic example.

Why can't you create new T() or new T[10]?

Because of erasure, the JVM doesn't know what T is at run time. Pass a Class<T> or a factory (Supplier<T>) instead, or use a List<T> rather than an array.

What's the difference between List<Object>, List<?> and raw List?

List<Object> holds any objects and you can add anything, but you can't pass it a List<String>. List<?> accepts a list of any type, but you can only read (as Object) and add null. A raw List turns off checking entirely, which is unsafe.

Can a generic class have static members that use T?

No. T belongs to each instance's type argument, but static members are shared by all instances (Box<String> and Box<Integer> are one class). Static generic methods declare their own type parameters instead: static <U> Box<U> of(U u).

Practice problems

Write and use generic classes and methods, and choose the right wildcard. The last three are business tasks.

P1Easy

A generic Box

Write Box<T> with put and get. Store a Double price and print it with 18% GST added.

Input
price 100.0
Expected output
118.0
Show solution and explanation
class Box<T> {
    private T item;
    void put(T item) { this.item = item; }
    T get() { return item; }
}

public class Main {
    public static void main(String[] args) {
        Box<Double> price = new Box<>();
        price.put(100.0);
        System.out.println(price.get() * 1.18);
    }
}
118.0

How it works: get() returns a Double (no cast); it's unboxed for the multiplication.

P2Easy

Generic method: first element

Write <T> T first(List<T> xs) and use it on a list of names and a list of numbers.

Input
["Riya", "Om"], [7, 3]
Expected output
Riya 7
Show solution and explanation
import java.util.List;

public class Main {
    static <T> T first(List<T> xs) {
        return xs.get(0);
    }

    public static void main(String[] args) {
        String n = first(List.of("Riya", "Om"));
        Integer x = first(List.of(7, 3));
        System.out.println(n + " " + x);
    }
}
Riya 7

How it works: the method's own type parameter is inferred at each call: String, then Integer.

P3Easy

Count elements of any list

Write static int count(List<?> xs) that works for any list, and call it with a list of Strings and a list of Doubles.

Input
["a", "b"], [1.5]
Expected output
2 1
Show solution and explanation
import java.util.List;

public class Main {
    static int count(List<?> xs) {
        return xs.size();
    }

    public static void main(String[] args) {
        System.out.println(count(List.of("a", "b")) + " " + count(List.of(1.5)));
    }
}
2 1

How it works: the unbounded wildcard ? accepts a list of any element type. We only call methods that don't depend on the type.

P4Medium

Bounded average

Write <T extends Number> double avg(List<T> xs) and test it with Integers and Doubles.

Input
[80, 90, 70], [2.5, 3.5]
Expected output
80.0 3.0
Show solution and explanation
import java.util.List;

public class Main {
    static <T extends Number> double avg(List<T> xs) {
        double s = 0;
        for (T x : xs) s += x.doubleValue();
        return s / xs.size();
    }

    public static void main(String[] args) {
        System.out.println(avg(List.of(80, 90, 70)) + " " + avg(List.of(2.5, 3.5)));
    }
}
80.0 3.0

How it works: the bound T extends Number lets us call doubleValue() on T.

P5Medium

Fill a list with ? super

Write addRange(List<? super Integer> dest, int from, int to) and use it to fill a List<Number> and a List<Object>.

Input
1..3
Expected output
[1, 2, 3] [1, 2, 3]
Show solution and explanation
import java.util.ArrayList;
import java.util.List;

public class Main {
    static void addRange(List<? super Integer> dest, int from, int to) {
        for (int i = from; i <= to; i++) dest.add(i);
    }

    public static void main(String[] args) {
        List<Number> nums = new ArrayList<>();
        List<Object> objs = new ArrayList<>();
        addRange(nums, 1, 3);
        addRange(objs, 1, 3);
        System.out.println(nums + " " + objs);
    }
}
[1, 2, 3] [1, 2, 3]

How it works: consumer super: any list whose element type is Integer or a supertype can safely receive Integers.

P6Business problem

Generic paginated results

An app shows 2 items per page. Write <T> List<T> page(List<T> all, int pageNo, int size) and print page 2 of five products.

Input
[P1..P5], page 2, size 2
Expected output
[P3, P4]
[P5]
Show solution and explanation
import java.util.List;

public class Main {
    static <T> List<T> page(List<T> all, int pageNo, int size) {
        int from = (pageNo - 1) * size;
        int to = Math.min(from + size, all.size());
        return from >= all.size() ? List.of() : all.subList(from, to);
    }

    public static void main(String[] args) {
        List<String> products = List.of("P1", "P2", "P3", "P4", "P5");
        System.out.println(page(products, 2, 2));
        System.out.println(page(products, 3, 2));
    }
}
[P3, P4] [P5]

How it works: one generic method pages products, orders or users alike. subList returns a view of the range.

P7Business problem

Typed cache with a size limit

Write Cache<K, V> that keeps at most 2 entries, evicting the oldest. Store three exchange rates and print the cache.

Input
USD 83.2, EUR 90.1, GBP 105.4
Expected output
{EUR=90.1, GBP=105.4} USD=null
Show solution and explanation
import java.util.LinkedHashMap;
import java.util.Map;

class Cache<K, V> {
    private final int max;
    private final LinkedHashMap<K, V> map = new LinkedHashMap<>();

    Cache(int max) { this.max = max; }

    void put(K key, V value) {
        map.put(key, value);
        if (map.size() > max) {
            K oldest = map.keySet().iterator().next();
            map.remove(oldest);
        }
    }

    V get(K key) { return map.get(key); }

    @Override
    public String toString() { return map.toString(); }
}

public class Main {
    public static void main(String[] args) {
        Cache<String, Double> rates = new Cache<>(2);
        rates.put("USD", 83.2);
        rates.put("EUR", 90.1);
        rates.put("GBP", 105.4);
        System.out.println(rates + " USD=" + rates.get("USD"));
    }
}
{EUR=90.1, GBP=105.4} USD=null

How it works: K and V are fixed per cache instance; LinkedHashMap keeps insertion order so the first key is the oldest.

P8Business problem

Validation result for any form

Write Validated<T> holding either a value or an error. Validate an age ("25" → 25) and a bad age ("abc").

Input
"25", "abc"
Expected output
OK 25
ERROR not a number: abc
Show solution and explanation
class Validated<T> {
    final T value;
    final String error;

    private Validated(T value, String error) { this.value = value; this.error = error; }

    static <T> Validated<T> ok(T v) { return new Validated<>(v, null); }
    static <T> Validated<T> bad(String e) { return new Validated<>(null, e); }

    @Override
    public String toString() { return error == null ? "OK " + value : "ERROR " + error; }
}

public class Main {
    static Validated<Integer> age(String text) {
        try {
            int a = Integer.parseInt(text);
            return a > 0 && a < 120 ? Validated.ok(a) : Validated.bad("out of range");
        } catch (NumberFormatException e) {
            return Validated.bad("not a number: " + text);
        }
    }

    public static void main(String[] args) {
        System.out.println(age("25"));
        System.out.println(age("abc"));
    }
}
OK 25 ERROR not a number: abc

How it works: the generic result type works for ages, emails or dates, and callers get a typed value with no casts.

Real-world scenario questions (asked in company interviews)

Generics questions in interviews range from “why no casts?” to designing reusable APIs. Tie your answer to safety and reuse.

Legacy code · bug huntFresher

A ClassCastException far from the bug

A report crashes with ClassCastException: Integer cannot be cast to String while reading a list of names. The list is a raw ArrayList filled by three different modules. How do you find and prevent the bug?

The interviewer is checking: raw types, unchecked warnings and moving errors to compile time.

Show a strong answer
  • Change the declaration to List<String> and the methods that pass it around. javac will flag the module that adds an Integer.
  • Compile with -Xlint:unchecked and fix every warning; add it to the build so new raw types fail.
  • For data from outside (files, JSON), convert and validate at the boundary.

Avoid: adding instanceof String checks in the report. That hides the bad data instead of stopping it.

Backend · API designJunior

Ten nearly identical response classes

A service has OrderResponse, UserResponse, ProductResponse… each with status, message and a data field of a different type. How would you simplify it?

The interviewer is checking: designing a generic class.

Show a strong answer
  • One ApiResponse<T> with status, message and T data, plus factory methods ok(T) and error(String).
  • Endpoints return ApiResponse<Order>, ApiResponse<List<Product>>: type-safe, one class to test.
  • JSON libraries handle generic types with a type token (e.g. Jackson's TypeReference) because of erasure.

Avoid: Object data. Every caller would need a cast and could get it wrong.

Library · method signatureMid

A sum method that won't accept List<Integer>

A teammate wrote double total(List<Number> amounts). Calling it with a List<Integer> doesn't compile. Why, and what's the fix?

The interviewer is checking: invariance and wildcards.

Show a strong answer
  • Generics are invariant: List<Integer> isn't a List<Number>, because the method could otherwise add a Double into a list of Integers.
  • The method only reads, so make it a producer: double total(List<? extends Number> amounts).
  • If it also needed to add, use a type parameter: <T extends Number> void addAll(List<T> xs, T x).

Avoid: overloading for every number type or using a raw List.

Platform · cachingJunior

A reusable in-memory cache

You need small caches for exchange rates (String → Double) and user profiles (Long → Profile), each with a maximum size. Sketch the class.

The interviewer is checking: multiple type parameters and composition.

Show a strong answer
  • class Cache<K, V> wrapping a LinkedHashMap<K, V> (insertion or access order) with get(K), put(K, V) and eviction of the oldest entry when full.
  • Usage: Cache<String, Double> rates, Cache<Long, Profile> profiles: one tested class.
  • For production, consider a library cache with expiry and thread safety.

Avoid: a Map<Object, Object> “universal cache” that needs casts at every call site.