Chapter 19: Generics
Generics let you write classes, interfaces, and methods that can work with any data type — while still keeping strong type checking at compile time.
Imagine we’re sitting together in a quiet Mumbai café — it’s evening, the city is glowing outside, and I’m going to explain generics like I’m teaching my younger brother who just discovered why ArrayList<String> is so much better than the old ArrayList without types.
We’ll go super slowly, with real-life analogies, complete runnable programs, step-by-step breakdowns, tables, common mistakes with fixes, and tons of examples you can copy-paste and run right now.
Let’s dive in!
1. Why Generics? (The Big Problem They Solve)
Before generics (pre-Java 5), collections were not type-safe:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
ArrayList list = new ArrayList(); list.add("Hello"); list.add(123); // Allowed — no compile error! list.add(new Student()); // Allowed! String s = (String) list.get(0); // OK Integer i = (Integer) list.get(1); // OK Student st = (Student) list.get(2); // OK // But if you do wrong cast? String wrong = (String) list.get(1); // Runtime ClassCastException! |
Problems:
- No compile-time type checking → bugs only appear at runtime
- Every get() needs manual casting → ugly and error-prone
- Mixing types accidentally → dangerous
Generics fix this:
|
0 1 2 3 4 5 6 7 8 9 |
ArrayList<String> names = new ArrayList<>(); names.add("Webliance"); // names.add(123); // Compile error! Type-safe! String name = names.get(0); // No cast needed — compiler knows it's String |
2. Generic Classes
A generic class uses a type parameter (usually T, E, K, V…) to represent any type.
Syntax:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Box<T> { private T item; public void setItem(T item) { this.item = item; } public T getItem() { return item; } } |
Usage:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class GenericClassDemo { public static void main(String[] args) { // Box for Strings Box<String> stringBox = new Box<>(); stringBox.setItem("Hello Webliance!"); String message = stringBox.getItem(); System.out.println(message); // Box for Integers Box<Integer> intBox = new Box<>(); intBox.setItem(2026); int year = intBox.getItem(); System.out.println("Year: " + year); // Box for custom class Box<Student> studentBox = new Box<>(); studentBox.setItem(new Student("Amit", 25)); } } |
Real-life analogy: Box<T> is like a gift box.
- You can put any gift inside (String, Integer, Student…)
- But when you open it, you know exactly what type is inside → no surprise!
3. Generic Methods
A generic method can have its own type parameter — independent of the class.
Syntax:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class Utils { // Generic method public static <T> void printArray(T[] array) { for (T element : array) { System.out.print(element + " "); } System.out.println(); } // Generic method with return type public static <T> T getFirst(T[] array) { return array.length > 0 ? array[0] : null; } } |
Usage:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class GenericMethodDemo { public static void main(String[] args) { Integer[] intArray = {10, 20, 30}; String[] strArray = {"Mumbai", "Delhi", "Pune"}; Utils.printArray(intArray); // 10 20 30 Utils.printArray(strArray); // Mumbai Delhi Pune String firstCity = Utils.getFirst(strArray); System.out.println("First city: " + firstCity); } } |
4. Bounded Types (Restrict the Type Parameter)
You can limit the type parameter to a specific class or interface.
Syntax:
|
0 1 2 3 4 5 6 7 |
<T extends Number> // T must be Number or subclass (Integer, Double, Float…) <T extends Comparable<T>> // T must implement Comparable |
Example: Generic class with bounded type
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Only numbers allowed public class NumberBox<T extends Number> { private T value; public void setValue(T value) { this.value = value; } public T getValue() { return value; } // Useful because T is Number → we can call doubleValue() public double doubleValue() { return value.doubleValue(); } } |
Usage:
|
0 1 2 3 4 5 6 7 8 9 10 |
NumberBox<Integer> intBox = new NumberBox<>(); intBox.setValue(100); System.out.println(intBox.doubleValue()); // 100.0 // NumberBox<String> wrong = new NumberBox<>(); // Compile error! |
5. Wildcards (The ? Magic)
Wildcards let you work with unknown types safely.
| Wildcard Type | Syntax | Meaning / Use Case |
|---|---|---|
| Unbounded wildcard | ? | Any type — read-only (safe for getting elements) |
| Upper bounded wildcard | ? extends Type | Any type that is Type or subclass of Type — read-only |
| Lower bounded wildcard | ? super Type | Any type that is Type or superclass of Type — write-only (add elements) |
Example: Upper bounded wildcard
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
public static void printNumbers(List<? extends Number> list) { for (Number n : list) { System.out.print(n + " "); } System.out.println(); // list.add(10); // Compile error! Cannot add — unknown exact type } |
Usage:
|
0 1 2 3 4 5 6 7 8 9 10 |
List<Integer> ints = List.of(1, 2, 3); List<Double> doubles = List.of(1.5, 2.5); printNumbers(ints); // Works printNumbers(doubles); // Works |
Example: Lower bounded wildcard (PECS rule)
|
0 1 2 3 4 5 6 7 8 9 10 |
public static void addNumbers(List<? super Integer> list) { list.add(10); list.add(20); // Can add Integer or its subclasses } |
PECS Rule (very important!):
- Producer Extends → use ? extends when you read from collection
- Consumer Super → use ? super when you write to collection
6. Quick Recap Table (Your Cheat Sheet)
| Concept | Syntax / Example | Purpose / Benefit |
|---|---|---|
| Generic Class | class Box<T> | Type-safe reusable container |
| Generic Method | <T> void print(T item) | Flexible methods for any type |
| Bounded Type | <T extends Number> | Restrict to specific types/interfaces |
| Unbounded Wildcard | List<?> | Accept any List (read-only) |
| Upper Bounded Wildcard | List<? extends Animal> | Read from list safely |
| Lower Bounded Wildcard | List<? super Dog> | Write to list safely |
7. Common Mistakes & Fixes
| Mistake | Problem | Fix |
|---|---|---|
| Using raw types (List instead of List<String>) | Lose type safety — compiler warnings | Always use generics: List<String> |
| List<?> and trying to add | Compile error — unknown type | Use List<? super T> if you want to write |
| Creating new T() inside generic class | Compile error — T might not have constructor | Use factory pattern or pass instance |
| Forgetting <> after new | Raw type warning | Use diamond operator: new ArrayList<>() |
| Mixing generics with arrays | Arrays don’t support generics well | Prefer ArrayList<T> over T[] |
8. Homework for You (Practice to Master!)
- Basic: Create generic class Pair<K, V> with two fields (key, value). Use it to store name-age pairs.
- Medium: Write generic method swap(T[] array, int i, int j) that swaps two elements.
- Advanced: Create generic method max(T[] array) that finds maximum element (T must implement Comparable<T>).
- Fun: Create generic class Stack<T> with push/pop methods using ArrayList<T>.
- Challenge: Fix this code — why does it fail and how to make it type-safe?
Java0123456789List list = new ArrayList();list.add("Hello");list.add(123);String s = (String) list.get(1); // Crash!
You’re doing amazing! Generics are the secret sauce that makes Java collections safe, readable, and powerful — now you can write professional, modern Java code.
