Core Java · Generics · Section 1 of 10

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.

Beginner-friendly
~8 min read
10 sections

Imagine a Box

Real-World Analogy

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.

OldWay.java ⚠ Without Generics
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());
        }
    }
}
Console output Runtime
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.

⚡ Why Runtime Errors Are Worse
A compile-time error is caught before your program ever runs. A runtime error happens in production, when real users are using your app. With the old approach, a bug like this might go unnoticed for months.

The Two Core Problems

Problem 1
You had to cast every value you read out of a collection. Tedious, error-prone, ugly code.
Problem 2
The compiler couldn't protect you. It happily accepted the wrong type and let you discover the mistake at runtime.
The Solution
Generics — introduced in Java 5. Type parameters that tell the compiler exactly what's allowed.

The Same Code With Generics

NewWay.java ✓ 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.

✓ The Core Promise of Generics
Move errors from runtime (your app crashes) to compile time (the compiler tells you immediately). Generics make your code type-safe.
Section 1 of 10
Core Java · Generics · Section 2 of 10

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.

class Box<T> { ... }
keyword
class name
type parameter
📐 What Does T Stand For?
T is just a convention — it stands for "Type". It's a variable name, but instead of holding a value, it holds a type. You could name it anything (like 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

1
You write a generic class or method once
Using T (or another letter) as a stand-in for any real type.
2
When you use it, you fill in the real type
e.g. Box<String> or Box<Integer> — T becomes String or Integer everywhere.
3
The compiler enforces it
From that point on, it checks every operation against the concrete type you provided.

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
📝 Type Erasure (For the Curious)
Under the hood, Java removes the generic type information when compiling — this is called type erasure. At runtime, 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.
Section 2 of 10
Core Java · Generics · Section 3 of 10

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:

Repetitive.java ⚠ Repetitive
void printStringArray(String[] arr) { ... }
void printIntegerArray(Integer[] arr) { ... }
void printDoubleArray(Double[] arr) { ... }
// ... same logic, three times

With a generic method, you write it once:

PrintAny.java ✓ Generic Method
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
    }
}
Output Console
Alice Bob Carol
1 2 3 4 5
9.99 14.49 3.0
📐 Syntax Breakdown
The <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:

Pair.java Example
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
    }
}
✓ No Cast Required
Notice we assign getFirst(words) directly to a String variable — no cast needed. The compiler knows the return type because it tracked T = String.
🧠 Quick Check
Where does <T> appear in a generic method declaration?
Section 3 of 10
Core Java · Generics · Section 4 of 10

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:

Box.java Generic Class
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;
    }
}
Main.java Usage
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
    }
}
💡 The Diamond Operator <>
On the right side of 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
⚠ Primitives Not Allowed
You cannot use primitive types as type parameters: Box<int>
Use the wrapper classes instead: Box<Integer>

Same for all primitives: doubleDouble, booleanBoolean, charCharacter, etc.
Section 4 of 10
Core Java · Generics · Section 5 of 10

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.

Pair.java Two Type Parameters
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 + ")";
    }
}
Main.java Usage
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

Real-World Connection

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.

MapExample.java Map uses K, V
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
    }
}
Section 5 of 10
Core Java · Generics · Section 6 of 10

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:

Broken.java ⚠ Won't Compile
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:

MaxFinder.java ✓ Bounded
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
    }
}
📐 Why "extends" for Interfaces?
In bounded type parameters, 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:

NumberStats.java Class Bound
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
    }
}
🧠 Quick Check
What does <T extends Comparable<T>> mean?
Section 6 of 10
Core Java · Generics · Section 7 of 10

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

Analogy

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:

Problem.java ⚠ Too Restrictive
// 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...)

? extends Number

Lower Bounded Wildcard

Accept any list of Number or a supertype of Number (Object...)

? super Integer

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

WildcardSum.java ✓ With Wildcard
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 ✓
    }
}
✓ PECS: The Rule for Wildcards
A common Java guideline:

Producer Extends, Consumer Super

If you're reading from a collection (it produces values) → use ? extends T
If you're writing to a collection (it consumes values) → use ? super T

Don't stress about PECS now — just know it exists. It becomes natural with experience.
Section 7 of 10
Core Java · Generics · Section 8 of 10

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.

CollectionsDemo.java Common Collections
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

Iteration.java All three styles
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 uses Generics too
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."
Section 8 of 10
Core Java · Generics · Section 9 of 10

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

Primitive.java ❌ Wrong
List<int> numbers = new ArrayList<>();  // ❌ int is a primitive
Primitive.java ✓ Fixed
List<Integer> numbers = new ArrayList<>();  // ✓ Integer is the wrapper

Mistake 2: Raw Types

Raw.java ❌ Raw Type
List names = new ArrayList();  // ❌ raw type — no type parameter
names.add("Alice");
names.add(42);  // compiles — dangerous!
Raw.java ✓ Fixed
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>

Subtype.java ❌ Wrong Assumption
class Animal {}
class Dog extends Animal {}

List<Dog> dogs = new ArrayList<>();

// ❌ Even though Dog extends Animal:
List<Animal> animals = dogs;  // COMPILE ERROR
Subtype.java ✓ Use Wildcard
// 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

Array.java ❌ Won't Compile
public class Container<T> {
    private T[] data = new T[10];  // ❌ cannot create generic array
}
Array.java ✓ Use ArrayList Instead
public class Container<T> {
    private List<T> data = new ArrayList<>();  // ✓ use a List instead
}
⚠ Quick Checklist
Before you call a generic "not working" — check these:

□  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?
Section 9 of 10
Core Java · Generics · Section 10 of 10

Quiz & Coding Challenge

Test your understanding and put it into practice.

Final Quiz

Question 1 of 3
Which of these is a valid generic type parameter?
Question 2 of 3
You write Box<String> b = new Box<>(). What does the <> (diamond) do?
Question 3 of 3
What does T extends Number in a type parameter mean?

Coding Challenges

🏋 Challenge 1 — Beginner
Generic Stack
Build a generic Stack<T> class that works like a real stack (last in, first out).
01 Create a Stack<T> class with a private ArrayList<T> as internal storage
02 Add a push(T item) method that adds to the top
03 Add a pop() method that removes and returns the top item
04 Add a peek() method that returns (but doesn't remove) the top item
05 Test it with Stack<String> and Stack<Integer> in main
Stack.java Solution
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(); }
}
🏋🏋 Challenge 2 — Intermediate
Generic Utility Methods
Write a utility class with generic static methods.
01 Write <T> void swap(T[] arr, int i, int j) — swaps two elements in any array
02 Write <T extends Comparable<T>> T min(List<T> list) — finds the minimum element
03 Write <T> List<T> repeat(T item, int n) — returns a list with the item repeated n times
Utils.java Solution
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;
    }
}
✓ What You've Learned
You now understand: why generics exist · type parameters T/E/K/V · generic methods and classes · multiple type parameters · bounded types with extends · wildcards ? extends / ? super · how collections use generics · common mistakes to avoid. Generics are one of the most important concepts in Java — everything else in the language builds on them.
Section 10 of 10
Roadmap