The Problem Generics Solve
Before learning what generics are, you need to feel the pain they eliminate. This section shows you the mess Java had before generics existed.
Imagine a Box
Imagine you have a physical box. You put a glass vase inside. You hand it to your friend and say "here's a box." Your friend reaches in, assumes it's a ball, squeezes — and the vase shatters.
This is exactly what Java without generics was like. The box didn't remember what type of thing it held. Mistakes happened at runtime, when it was too late.
The Pre-Generics World (Before Java 5)
Imagine you want a list that holds Strings. Before generics, you'd use the raw ArrayList — which held Object, meaning anything.
import java.util.ArrayList; public class OldWay { public static void main(String[] args) { ArrayList names = new ArrayList(); // raw type — no <String> names.add("Alice"); names.add("Bob"); names.add(42); // 😬 This compiles fine — no error! // Now we loop and try to treat everything as a String: for (Object item : names) { String name = (String) item; // cast required every time System.out.println(name.toUpperCase()); } } }
ALICE
BOB
Exception in thread "main" java.lang.ClassCastException:
class java.lang.Integer cannot be cast to class java.lang.String
The program compiled without errors. It only crashed when it ran — on the line with the integer. This is a runtime error, the worst kind.
The Two Core Problems
The Same Code With Generics
import java.util.ArrayList; public class NewWay { public static void main(String[] args) { ArrayList<String> names = new ArrayList<>(); names.add("Alice"); names.add("Bob"); // names.add(42); ← ❌ COMPILE ERROR: incompatible types for (String name : names) { // no cast needed System.out.println(name.toUpperCase()); } } }
Now the compiler knows the list only holds Strings. Try to add an integer and it refuses to compile. The error is caught immediately, before you ever run anything.
What Is a Generic?
A generic is a class, method, or interface that works with a type you specify instead of hardcoding a specific type like String or Integer.
The Anatomy of a Generic
The key symbol is the angle brackets <T>. The T is a type parameter — a placeholder for whatever type you choose when you use the class or method.
Banana), but the community uses single uppercase letters:
T = Type · E = Element (collections) · K = Key · V = Value · N = Number
How It Works Step by Step
T (or another letter) as a stand-in for any real type.Box<String> or Box<Integer> — T becomes String or Integer everywhere.A Simple Comparison
| Without Generics | With Generics |
|---|---|
| Raw type — holds any Object | Typed — holds exactly what you declare |
| Requires manual cast when reading | No cast needed — type is already known |
| Errors discovered at runtime (crashes) | Errors caught at compile time (before running) |
| One class written per type needed | One class works for any type |
ArrayList<String> and ArrayList<Integer> are both just ArrayList. Generics are purely a compile-time safety net. You don't need to understand type erasure deeply right now — just know it exists.
Generic Methods
A generic method introduces its own type parameter, independent of any class. It's the simplest place to start writing generics yourself.
The Problem: Repeating Code for Each Type
Suppose you want a method that prints any array. Without generics, you'd write a separate version for each type:
void printStringArray(String[] arr) { ... } void printIntegerArray(Integer[] arr) { ... } void printDoubleArray(Double[] arr) { ... } // ... same logic, three times
With a generic method, you write it once:
public class PrintAny { // <T> before the return type declares the type parameter public static <T> void printArray(T[] array) { for (T element : array) { System.out.print(element + " "); } System.out.println(); } public static void main(String[] args) { String[] names = {"Alice", "Bob", "Carol"}; Integer[] numbers = {1, 2, 3, 4, 5}; Double[] prices = {9.99, 14.49, 3.00}; printArray(names); // T becomes String printArray(numbers); // T becomes Integer printArray(prices); // T becomes Double } }
Alice Bob Carol 1 2 3 4 5 9.99 14.49 3.0
<T> appears before the return type in a generic method. This declares the type parameter for that method.public static <T> void printArray(T[] array)The
<T> on the left says "T is a type parameter". The T on the right uses it.
Return Values from Generic Methods
Generic methods can also return the type parameter:
public class ArrayUtils { // Returns the first element of any array public static <T> T getFirst(T[] array) { return array[0]; } public static void main(String[] args) { String[] words = {"hello", "world"}; Integer[] nums = {10, 20, 30}; String firstWord = getFirst(words); // returns String Integer firstNum = getFirst(nums); // returns Integer System.out.println(firstWord); // hello System.out.println(firstNum); // 10 } }
getFirst(words) directly to a String variable — no cast needed. The compiler knows the return type because it tracked T = String.
<T> appear in a generic method declaration?Generic Classes
A generic class defines a blueprint that works for any type. When you create an instance, you lock in the type for that object.
Building a Box Class
Let's build the classic example — a Box that holds one item of any type:
public class Box<T> { private T content; // T used as a field type public void put(T item) { // T used as parameter type this.content = item; } public T get() { // T used as return type return content; } public boolean isEmpty() { return content == null; } }
public class Main { public static void main(String[] args) { // A box that holds Strings Box<String> nameBox = new Box<>(); nameBox.put("Alice"); String name = nameBox.get(); // no cast — already a String System.out.println(name); // Alice // A box that holds Integers Box<Integer> ageBox = new Box<>(); ageBox.put(30); Integer age = ageBox.get(); System.out.println(age); // 30 // This won't compile — String box can't accept Integer: // nameBox.put(42); ← ❌ compile error } }
new Box<>() you see empty angle brackets. This is the diamond operator (Java 7+). Java infers the type from the left side, so you don't have to repeat it. Box<String> box = new Box<>(); ← Java knows T = String from the left.
What Does T "Become"?
Each time you create a new instance with a specific type, T is replaced throughout the class. Think of it as a find-and-replace:
| You write | T becomes | Effective class looks like |
|---|---|---|
Box<String> |
String |
fields and methods use String |
Box<Integer> |
Integer |
fields and methods use Integer |
Box<Double> |
Double |
fields and methods use Double |
Box<int> ❌Use the wrapper classes instead:
Box<Integer> ✓Same for all primitives:
double → Double, boolean → Boolean, char → Character, etc.
Multiple Type Parameters
A class or method can declare more than one type parameter. You'll see this constantly in real Java code — Map<K, V> is a classic example.
Declaring Multiple Type Params
Separate them with a comma inside the angle brackets. The most common names are K (key) and V (value), but any letters work.
public class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } @Override public String toString() { return "(" + key + ", " + value + ")"; } }
public class Main { public static void main(String[] args) { // K = String, V = Integer Pair<String, Integer> student = new Pair<>("Alice", 95); System.out.println(student); // (Alice, 95) System.out.println(student.getKey()); // Alice System.out.println(student.getValue()); // 95 // K = String, V = String Pair<String, String> cityCountry = new Pair<>("Paris", "France"); System.out.println(cityCountry); // (Paris, France) // K = Integer, V = Boolean Pair<Integer, Boolean> flagged = new Pair<>(404, true); System.out.println(flagged); // (404, true) } }
This Is Exactly How Map Works
Java's built-in Map<K, V> interface uses exactly this pattern. When you write Map<String, Integer>, you're saying: "Keys are Strings, values are Integers." The K and V in the Map source code are the same concept you just built.
import java.util.HashMap; import java.util.Map; public class MapExample { public static void main(String[] args) { // HashMap<K, V> — K=String key, V=Integer value Map<String, Integer> scores = new HashMap<>(); scores.put("Alice", 90); scores.put("Bob", 75); Integer aliceScore = scores.get("Alice"); // no cast needed System.out.println(aliceScore); // 90 } }
Bounded Type Parameters
Sometimes you want to restrict T to a specific family of types. Bounded parameters let you say "T must be a Number" or "T must implement Comparable."
The Problem: T Is Too Open
Suppose you write a generic method to find the max of two values. With an unrestricted T, you can't compare them — not all objects support comparison:
public static <T> T max(T a, T b) { // ❌ ERROR: cannot compare — T might be anything return (a > b) ? a : b; }
Upper Bounded: extends
Use T extends SomeType to restrict T to that type or any subclass/implementation of it:
public class MaxFinder { // T must implement Comparable<T> public static <T extends Comparable<T>> T max(T a, T b) { return (a.compareTo(b) > 0) ? a : b; } public static void main(String[] args) { // Integer implements Comparable — works ✓ System.out.println(max(10, 20)); // 20 // String implements Comparable — works ✓ System.out.println(max("apple", "banana")); // banana // Double implements Comparable — works ✓ System.out.println(max(3.14, 2.71)); // 3.14 } }
extends is used even for interfaces — not implements. This is a Java quirk. T extends Comparable<T> means "T must implement the Comparable interface." Think of it as "T must be a subtype of Comparable."
Bounding with a Class
You can also bound T to a class hierarchy. This lets you call methods defined on that class:
public class NumberStats { // T must be a Number (Integer, Double, Long, etc. are all Numbers) public static <T extends Number> double sum(T[] numbers) { double total = 0; for (T n : numbers) { total += n.doubleValue(); // doubleValue() is on Number } return total; } public static void main(String[] args) { Integer[] ints = {1, 2, 3, 4}; Double[] doubles = {1.5, 2.5, 3.0}; System.out.println(sum(ints)); // 10.0 System.out.println(sum(doubles)); // 7.0 // sum(new String[]{"a","b"}); ← ❌ String is not a Number } }
<T extends Comparable<T>> mean?Wildcards
The wildcard ? means "some unknown type." It appears when you're working with generic collections and you need flexibility without binding to a specific T.
The Problem Wildcards Solve
Imagine a food bank that accepts donations. It doesn't care if you bring canned soup, canned corn, or canned beans — as long as it's canned food. In Java terms: it accepts List<? extends CannedFood>.
Here's the concrete problem. Suppose you write a method to sum a list of numbers. Intuitively, it should work for List<Integer>, List<Double>, and List<Long>. But in Java, these are different types:
// This only accepts List<Number> — NOT List<Integer> public static double sum(List<Number> list) { ... } List<Integer> ints = List.of(1, 2, 3); sum(ints); // ❌ COMPILE ERROR — List<Integer> is not a List<Number>
This surprises beginners. Even though Integer extends Number, List<Integer> does not extend List<Number>. This is by design — changing it would break type safety. Wildcards are the solution.
Upper Bounded Wildcard
Accept any list of Number or a subtype of Number (Integer, Double, Long...)
Lower Bounded Wildcard
Accept any list of Number or a supertype of Number (Object...)
Unbounded Wildcard
Accept a list of any type — use when you don't care about the type at all (e.g. just printing sizes)
Upper Bounded Wildcard in Practice
import java.util.List; public class WildcardSum { // ? extends Number = any list whose element type is Number or a subtype public static double sum(List<? extends Number> list) { double total = 0; for (Number n : list) { total += n.doubleValue(); } return total; } public static void main(String[] args) { List<Integer> ints = List.of(1, 2, 3); List<Double> doubles = List.of(1.5, 2.5); List<Long> longs = List.of(100L, 200L); System.out.println(sum(ints)); // 6.0 ✓ System.out.println(sum(doubles)); // 4.0 ✓ System.out.println(sum(longs)); // 300.0 ✓ } }
Producer Extends, Consumer Super
If you're reading from a collection (it produces values) → use
? extends TIf you're writing to a collection (it consumes values) → use
? super TDon't stress about PECS now — just know it exists. It becomes natural with experience.
Generics & Collections
Everything in the Java Collections Framework uses generics. This is where you'll use generics most in real code.
The Collections Framework Is All Generics
Every major collection — List, Set, Map, Queue — is a generic class or interface. Once you understand generics, the entire collections API clicks.
import java.util.*; public class CollectionsDemo { public static void main(String[] args) { // List<E> — ordered, duplicates allowed List<String> fruits = new ArrayList<>(); fruits.add("apple"); fruits.add("banana"); fruits.add("apple"); // duplicates OK // Set<E> — no duplicates Set<String> unique = new HashSet<>(fruits); System.out.println(unique); // [apple, banana] // Map<K, V> — key/value pairs Map<String, Integer> stock = new HashMap<>(); stock.put("apple", 50); stock.put("banana", 30); System.out.println(stock.get("apple")); // 50 // Queue<E> — FIFO processing Queue<String> tasks = new LinkedList<>(); tasks.offer("task1"); tasks.offer("task2"); System.out.println(tasks.poll()); // task1 (first in, first out) } }
Reading the Javadoc with Generics Eyes
When you look up ArrayList in the Java documentation, you'll see ArrayList<E>. That E is the element type parameter. Every method uses it:
| Method signature in docs | Meaning |
|---|---|
boolean add(E e) |
add an element of type E |
E get(int index) |
returns an element of type E |
boolean contains(Object o) |
check if element exists (uses Object for historical reasons) |
void sort(Comparator<? super E> c) |
sort using a comparator — you'll see this wildcard pattern often |
Iterating with Generics
List<String> cities = List.of("London", "Tokyo", "Berlin"); // 1. Enhanced for-loop (most common) for (String city : cities) { System.out.println(city); } // 2. forEach with lambda (modern) cities.forEach(city -> System.out.println(city)); // 3. Iterator (old school, still useful) Iterator<String> it = cities.iterator(); while (it.hasNext()) { System.out.println(it.next()); }
Collections.sort(List<T extends Comparable<? super T>> list) — this looks intimidating but you now know enough to read it: "sort a list where T can be compared to itself or a supertype."
Common Mistakes
Generics trip up beginners in predictable ways. Here are the most common errors you'll encounter and exactly how to fix them.
Mistake 1: Using a Primitive as a Type Parameter
List<int> numbers = new ArrayList<>(); // ❌ int is a primitive
List<Integer> numbers = new ArrayList<>(); // ✓ Integer is the wrapper
Mistake 2: Raw Types
List names = new ArrayList(); // ❌ raw type — no type parameter names.add("Alice"); names.add(42); // compiles — dangerous!
List<String> names = new ArrayList<>(); // ✓ always specify the type names.add("Alice"); // names.add(42); ← ❌ compiler catches it now
Mistake 3: Assuming List<Dog> is a List<Animal>
class Animal {} class Dog extends Animal {} List<Dog> dogs = new ArrayList<>(); // ❌ Even though Dog extends Animal: List<Animal> animals = dogs; // COMPILE ERROR
// Use ? extends Animal if you only need to read from the list List<? extends Animal> animals = dogs; // ✓ works
Mistake 4: Trying to Create a Generic Array
public class Container<T> { private T[] data = new T[10]; // ❌ cannot create generic array }
public class Container<T> { private List<T> data = new ArrayList<>(); // ✓ use a List instead }
□ Are you using wrapper types (
Integer, not int)?□ Did you declare the type parameter on the class/method?
□ Are you assuming a parameterized List is a subtype of another?
□ Are you trying to instantiate a generic array?
Quiz & Coding Challenge
Test your understanding and put it into practice.
Final Quiz
Box<String> b = new Box<>(). What does the <> (diamond) do?T extends Number in a type parameter mean?Coding Challenges
Stack<T> class that works like a real stack (last in, first out).Stack<T> class with a private ArrayList<T> as internal storage
push(T item) method that adds to the top
pop() method that removes and returns the top item
peek() method that returns (but doesn't remove) the top item
Stack<String> and Stack<Integer> in main
import java.util.ArrayList; public class Stack<T> { private ArrayList<T> items = new ArrayList<>(); public void push(T item) { items.add(item); } public T pop() { if (items.isEmpty()) throw new RuntimeException("Stack is empty"); return items.remove(items.size() - 1); } public T peek() { if (items.isEmpty()) throw new RuntimeException("Stack is empty"); return items.get(items.size() - 1); } public boolean isEmpty() { return items.isEmpty(); } public int size() { return items.size(); } }
<T> void swap(T[] arr, int i, int j) — swaps two elements in any array
<T extends Comparable<T>> T min(List<T> list) — finds the minimum element
<T> List<T> repeat(T item, int n) — returns a list with the item repeated n times
import java.util.*; public class Utils { public static <T> void swap(T[] arr, int i, int j) { T temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } public static <T extends Comparable<T>> T min(List<T> list) { T result = list.get(0); for (T item : list) { if (item.compareTo(result) < 0) result = item; } return result; } public static <T> List<T> repeat(T item, int n) { List<T> result = new ArrayList<>(); for (int i = 0; i < n; i++) result.add(item); return result; } }