Generics
Dashboard
Topic 18/30

Generics in Java

Generics were introduced in Java 5 to provide compile-time type safety and eliminate the need for explicit type casting. They allow classes, interfaces, and methods to operate on objects of various types while providing compile-time type checks.

Why Generics?

  • Type Safety: We can hold only a single type of objects in generics. It doesn't allow to store other objects.
  • Type Casting is not required: There is no need to typecast the object.
  • Compile-Time Checking: It is checked at compile time so problems will not occur at runtime.
Note: Generics only work with Reference types, not primitive types. You cannot use int, but you can use Integer.

Type Parameters

Commonly used type parameter names are single, uppercase letters. These stand in for the type argument provided by the user.

LetterMeaning
TType
EElement (used extensively by the Java Collections Framework)
KKey
VValue
NNumber

Generic Classes

A generic class is defined with a generic type parameter list, enclosed in angle brackets <>. Let's create a generic Box class.

GenericBox.javaJava
public class GenericBox<T> {
    private T item;

    public void setItem(T item) {
        this.item = item;
    }

    public T getItem() {
        return item;
    }

    public static void main(String[] args) {
        // Box for Integer
        GenericBox<Integer> intBox = new GenericBox<>();
        intBox.setItem(100);
        System.out.println("Integer Value: " + intBox.getItem());

        // Box for String
        GenericBox<String> strBox = new GenericBox<>();
        strBox.setItem("Generics are awesome");
        System.out.println("String Value: " + strBox.getItem());
    }
}
Output
Integer Value: 100 String Value: Generics are awesome

Multiple Type Parameters

A generic class can have multiple type parameters. Here's a generic Pair class using K and V.

GenericPair.javaJava
public class GenericPair<K, V> {
    private K key;
    private V value;

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

    public K getKey() { return key; }
    public V getValue() { return value; }

    public static void main(String[] args) {
        GenericPair<String, Integer> pair1 = new GenericPair<>("Age", 30);
        System.out.println(pair1.getKey() + ": " + pair1.getValue());

        GenericPair<Integer, String> pair2 = new GenericPair<>(404, "Not Found");
        System.out.println("Error " + pair2.getKey() + ": " + pair2.getValue());
    }
}

Generic Methods and Data Structures

Let's build a Generic Stack data structure to understand how generics help build reusable components.

GenericStack.javaJava
import java.util.ArrayList;
import java.util.EmptyStackException;

public class GenericStack<T> {
    private ArrayList<T> elements = new ArrayList<>();

    public void push(T item) {
        elements.add(item);
    }

    public T pop() {
        if (elements.isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.remove(elements.size() - 1);
    }

    public boolean isEmpty() {
        return elements.isEmpty();
    }

    public static void main(String[] args) {
        GenericStack<Double> stack = new GenericStack<>();
        stack.push(10.5);
        stack.push(20.2);
        
        System.out.println("Popped: " + stack.pop());
        System.out.println("Popped: " + stack.pop());
    }
}

Bounded Type Parameters

Sometimes you want to restrict the types that can be used as type arguments. For example, a method that operates on numbers might only want to accept instances of Number or its subclasses. This is what bounded type parameters are for.

BoundedGenericDemo.javaJava
public class BoundedGenericDemo {
    // This method only accepts elements that extend Number
    public static <T extends Number> double sum(T num1, T num2) {
        return num1.doubleValue() + num2.doubleValue();
    }

    public static void main(String[] args) {
        System.out.println("Sum of Integers: " + sum(10, 20));
        System.out.println("Sum of Doubles: " + sum(15.5, 4.5));
        
        // sum("Hello", "World"); // Compile-time error! String is not a Number
    }
}
Multiple Bounds: A type variable can have multiple bounds, e.g., <T extends ClassA & InterfaceB & InterfaceC>. If one of the bounds is a class, it must be specified first.

Wildcards

In generic code, the question mark ?, called the wildcard, represents an unknown type. There are three types of wildcards:

  • Unbounded Wildcards <?>: Matches any type. Useful when methods don't depend on the type parameter.
  • Upper Bounded Wildcards <? extends Type>: Matches the type or any of its subclasses.
  • Lower Bounded Wildcards <? super Type>: Matches the type or any of its superclasses.
WildcardDemo.javaJava
import java.util.Arrays;
import java.util.List;

public class WildcardDemo {
    // Upper Bounded Wildcard - Accepts List of Number or its subclasses (Integer, Double, etc.)
    public static void printNumbers(List<? extends Number> list) {
        for (Number n : list) {
            System.out.print(n + " ");
        }
        System.out.println();
    }

    // Unbounded Wildcard - Accepts List of any type
    public static void printAnything(List<?> list) {
        for (Object obj : list) {
            System.out.print(obj + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        List<Integer> ints = Arrays.asList(1, 2, 3);
        List<Double> doubles = Arrays.asList(1.1, 2.2);
        List<String> strings = Arrays.asList("A", "B");

        System.out.print("Integers: ");
        printNumbers(ints);
        
        System.out.print("Doubles: ");
        printNumbers(doubles);

        // printNumbers(strings); // Compilation error
        
        System.out.print("Strings: ");
        printAnything(strings); // Unbounded wildcard accepts String
    }
}

Type Erasure & Raw Types

Generics were introduced to Java to provide tighter type checks at compile time. To implement generics, the Java compiler applies type erasure to:

  • Replace all type parameters with their bounds or Object if unbounded.
  • Insert type casts if necessary to preserve type safety.
  • Generate bridge methods to preserve polymorphism in extended generic types.
Because of this, List<String> and List<Integer> have the same class at runtime.

Raw Types: Using a generic type without type arguments (e.g., List list = new ArrayList();) is called a raw type. Raw types bypass generic type checks and can lead to ClassCastException at runtime. You should avoid them in modern Java.