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.
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?
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.
And I don't have to cast when I read?
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.
What are generics?
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.
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
T in class Box<T>.String in Box<String>.new ArrayList<>(): javac infers the type argument (Java 7+).T: T extends Comparable<T> means T must be comparable.? = some unknown type. List<? extends Number> = a list of some kind of Number.List): legacy, unsafe, gives warnings.| Naming convention | T type, E element, K/V key/value, R result |
|---|---|
| Type arguments | Reference types only: List<Integer>, never List<int> |
| Checked | At compile time; at run time List<String> and List<Integer> are the same class |
| Not allowed | new 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.
Four ways generics appear in code
1. Using generic types
List<String>, Map<String, Integer>: the everyday case.
2. Generic classes
class Box<T>: write once, use with any type.
3. Generic methods
<T> T first(List<T> xs): type decided per call.
4. Bounds and wildcards
T extends Number, ? extends, ? super: flexible but safe.
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
}
}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
}
}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);
}
}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 youRaw 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.
What javac does with List<String>
Check every call against the type argument
javac substitutesList<String> names = new ArrayList<>(); names.add(21007); // checked: int → String? No.E = Stringintoadd(E e)and reports incompatible types: int cannot be converted to String. No class file is written.Infer the diamond
new ArrayList<>()on the right becomesnew ArrayList<String>()from the variable's type on the left.Erase the type arguments
In the bytecode,List<String>becomes plainList,EbecomesObject(or its bound, e.g.ComparableforT extends Comparable<T>).Insert casts where values come out
String s = names.get(0)compiles to(String) names.get(0), acheckcastinstruction. It can't fail, because step 1 guaranteed only Strings went in.At run time, one class for all
new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass()istrue. The object on the heap has anObject[]inside, whatever the type argument was. Erasure kept Java 5 code compatible with older libraries.
Checked, then erased: both views
Left: the code you write with generics. Right: what javac checks, and what remains in the class file.
- The variable is declared
List<String>; the diamond on the right is filled in by javac. - Adding a String is fine.
- Adding
21007is a compile error. The bug is caught where it's written. names.get(0)is used as a String directly. No(String)cast in your code.- With the bad line removed, javac produces the class file.
- The program runs and prints the name in capitals.
- javac reads
List<String>and substitutesE = Stringinto every method ofListfor checking. add(E)is checked asadd(String): "Riya" fits.- An
intdoesn't fitadd(String), and it can't be boxed to String either. javac stops with an error. getreturnsE, which is String, so assigning it to a String needs no cast in your code.- Erasure: the class file calls
add(Object)andgetreturning Object, plus acheckcast Stringjavac inserted. It can't fail, because only Strings could get in. - At run time the list is a plain
ArrayListwith anObject[]inside.ArrayList<Integer>would be exactly the same class.
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.
- The team writes one class with a type parameter
Tfor the payload. - The order endpoint returns
ApiResponse<Order>:datais an Order, checked by javac. - The home screen gets
ApiResponse<List<Shop>>. Same class, different type argument. - When something fails,
okis false anderrorexplains why. Every endpoint answers in one predictable shape. (Simplified: real APIs also send status codes and request IDs.)
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.
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.
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.
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.
Examples are simplified; framework class names are given as common examples.
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());
}
}
}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 whatvalue()returns: an Integer, no cast.Result.fail("…")T is inferred from the method's return type.
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.
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).
[34, 71, 12] and ["Om", "Riya", "Kabir"]
71 Riya
Approach
- Declare
<T extends Comparable<T>> T best(List<T> xs): the bound allowscompareTo. - Loop and keep the larger element.
- 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.
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.
bills [499, 250], refunds [99.5]
Bills 749.0, refunds 99.5
Approach
- Parameter type
List<? extends Number>: a producer of Numbers. - Read each element as a Number and add
doubleValue(). - 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.
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).
(Riya, 'A'), (Kabir, 78)
Riya=A Kabir=78 A 80
Approach
class Pair<K, V>with two final fields and atoString.- Create
Pair<String, Character>andPair<String, Integer>. - Read
valueback 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.
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 Tto read (producer),? super Tto write (consumer): PECS. - Invariant:
List<String>is not aList<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.
A generic Box
Write Box<T> with put and get. Store a Double price and print it with 18% GST added.
price 100.0
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);
}
}How it works: get() returns a Double (no cast); it's unboxed for the multiplication.
Generic method: first element
Write <T> T first(List<T> xs) and use it on a list of names and a list of numbers.
["Riya", "Om"], [7, 3]
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);
}
}How it works: the method's own type parameter is inferred at each call: String, then Integer.
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.
["a", "b"], [1.5]
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)));
}
}How it works: the unbounded wildcard ? accepts a list of any element type. We only call methods that don't depend on the type.
Bounded average
Write <T extends Number> double avg(List<T> xs) and test it with Integers and Doubles.
[80, 90, 70], [2.5, 3.5]
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)));
}
}How it works: the bound T extends Number lets us call doubleValue() on T.
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>.
1..3
[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);
}
}How it works: consumer super: any list whose element type is Integer or a supertype can safely receive Integers.
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.
[P1..P5], page 2, size 2
[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));
}
}How it works: one generic method pages products, orders or users alike. subList returns a view of the range.
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.
USD 83.2, EUR 90.1, GBP 105.4
{EUR=90.1, GBP=105.4} USD=nullShow 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"));
}
}How it works: K and V are fixed per cache instance; LinkedHashMap keeps insertion order so the first key is the oldest.
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").
"25", "abc"
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"));
}
}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.
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:uncheckedand 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.
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>withstatus,messageandT data, plus factory methodsok(T)anderror(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.
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 aList<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.
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 aLinkedHashMap<K, V>(insertion or access order) withget(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.