Java Generics

Generics allow classes, interfaces, and methods to operate on typed parameters — meaning the type (like Integer, String, or any class) is specified at compile time, not at runtime. This makes code more flexible, type-safe, and reusable.

Without generics, a collection could hold any object, and a ClassCastException could occur at runtime. With generics, type errors are caught at compile time.

Why Use Generics?

  • Type safety: Prevents storing wrong types in a collection.
  • Eliminates casting: No need to manually cast objects when retrieving them.
  • Code reuse: Write one class or method that works for multiple types.

Generics Without vs With

Without Generics (Unsafe)

import java.util.ArrayList;

ArrayList list = new ArrayList();   // raw type
list.add("Hello");
list.add(42);   // different types – no compile-time error

String s = (String) list.get(1);   // runtime ClassCastException!

With Generics (Safe)

import java.util.ArrayList;

ArrayList<String> list = new ArrayList<>();
list.add("Hello");
// list.add(42);   // compile-time error – only Strings allowed

String s = list.get(0);   // no casting needed

Generic Classes

A class can be made generic by declaring a type parameter in angle brackets after the class name. The type parameter acts as a placeholder for the actual type provided when the class is used.

Example – A Generic Box

class Box<T> {
    private T value;

    Box(T value) {
        this.value = value;
    }

    T getValue() {
        return value;
    }

    void setValue(T value) {
        this.value = value;
    }
}

public class Main {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>("Java Generics");
        Box<Integer> intBox = new Box<>(100);
        Box<Double> doubleBox = new Box<>(3.14);

        System.out.println("String box: " + stringBox.getValue());
        System.out.println("Integer box: " + intBox.getValue());
        System.out.println("Double box: " + doubleBox.getValue());
    }
}

Output:

String box: Java Generics
Integer box: 100
Double box: 3.14

The same Box class works for any type. The type is determined when the object is created.

Multiple Type Parameters

A generic class can have more than one type parameter.

class Pair<K, V> {
    K key;
    V value;

    Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    void display() {
        System.out.println("Key: " + key + " | Value: " + value);
    }
}

public class Main {
    public static void main(String[] args) {
        Pair<String, Integer> student = new Pair<>("Alice", 95);
        Pair<Integer, String> code = new Pair<>(404, "Not Found");

        student.display();
        code.display();
    }
}

Output:

Key: Alice | Value: 95
Key: 404 | Value: Not Found

Generic Methods

A single method can be made generic independently of its class. The type parameter is declared before the return type.

public class GenericMethods {

    static <T> void printArray(T[] array) {
        for (T item : array) {
            System.out.print(item + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Integer[] ints = {1, 2, 3, 4, 5};
        String[] words = {"Hello", "Java", "World"};
        Double[] decimals = {1.1, 2.2, 3.3};

        printArray(ints);
        printArray(words);
        printArray(decimals);
    }
}

Output:

1 2 3 4 5 
Hello Java World 
1.1 2.2 3.3 

Bounded Type Parameters

Sometimes a type parameter should be restricted to a certain class or its subclasses. This is done using the extends keyword (an upper bound).

static <T extends Number> double sum(T[] array) {
    double total = 0;
    for (T item : array) {
        total += item.doubleValue();
    }
    return total;
}

public static void main(String[] args) {
    Integer[] ints = {10, 20, 30};
    Double[] doubles = {1.5, 2.5, 3.5};

    System.out.println("Sum of ints: " + sum(ints));       // 60.0
    System.out.println("Sum of doubles: " + sum(doubles)); // 7.5
    // sum(new String[]{"a","b"}); // compile error – String is not a Number
}

Wildcard – The ? Symbol

The wildcard ? represents an unknown type. It is used in method parameters when the exact type is unknown or flexible.

Unbounded Wildcard

static void printList(ArrayList<?> list) {
    for (Object item : list) {
        System.out.print(item + " ");
    }
    System.out.println();
}

Upper-Bounded Wildcard (? extends Type)

Accepts any type that is a subtype of the specified type.

static double totalArea(ArrayList<? extends Shape> shapes) {
    double total = 0;
    for (Shape s : shapes) {
        total += s.area();
    }
    return total;
}

Lower-Bounded Wildcard (? super Type)

Accepts any type that is a supertype of the specified type.

static void addNumbers(ArrayList<? super Integer> list) {
    list.add(10);
    list.add(20);
}

Generics with Interfaces

interface Comparable<T> {
    int compareTo(T other);
}

class Temperature implements Comparable<Temperature> {
    double value;

    Temperature(double value) { this.value = value; }

    @Override
    public int compareTo(Temperature other) {
        return Double.compare(this.value, other.value);
    }
}

Common Generic Type Parameter Names

SymbolConvention
TType (general purpose)
EElement (used in collections)
KKey (used in maps)
VValue (used in maps)
NNumber

Summary

  • Generics allow classes, interfaces, and methods to work with any data type safely.
  • Type parameters are declared using angle brackets: <T>, <K, V>.
  • Generics provide compile-time type checking, eliminating runtime ClassCastException.
  • Bounded type parameters use extends to restrict the accepted types.
  • The wildcard ? represents an unknown type and is used in flexible method signatures.
  • All Java collection classes (ArrayList, HashMap, etc.) use generics internally.

Leave a Comment

Your email address will not be published. Required fields are marked *