Skip to content

Java Generics

Generics is a powerful feature introduced in Java SE 5.0 that allows us to use type parameters when defining classes, interfaces, and methods. Using generics enables writing more flexible, safer, and clearer code.

Why Do We Need Generics?

Before generics were introduced, Java's collection classes (like ArrayList) could only store objects of type Object. This approach had two main problems:

  1. Type unsafe: You could add any type of object to an ArrayList, but errors couldn't be detected at compile time.
    java
    // Without generics
    List list = new ArrayList();
    list.add("Hello");
    list.add(123); // No compile-time error
  2. Required explicit type casting: When retrieving elements from a collection, you had to manually cast them to the expected type, which was cumbersome and could throw ClassCastException at runtime.
    java
    String first = (String) list.get(0);
    String second = (String) list.get(1); // Runtime error! ClassCastException

Generics perfectly solved these problems by moving type checking from runtime to compile time.

java
// Using generics
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
// stringList.add(123); // Compile error! Type mismatch

String first = stringList.get(0); // No casting needed

Generic Classes

We can define our own generic classes. Type parameters (usually represented by single uppercase letters, such as T for Type, E for Element, K for Key, V for Value) are declared in angle brackets <> after the class name.

java
// Define a generic class Box
public class Box<T> {
    private T content;

    public void setContent(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }
}

// Using the generic class
public class Main {
    public static void main(String[] args) {
        // Create a Box that stores String
        Box<String> stringBox = new Box<>();
        stringBox.setContent("This is a string");
        System.out.println(stringBox.getContent());

        // Create a Box that stores Integer
        Box<Integer> integerBox = new Box<>();
        integerBox.setContent(100);
        System.out.println(integerBox.getContent());
    }
}

Generic Methods

Besides generic classes, we can also define generic methods. Generic methods have their own type parameters, which are declared before the method return type.

java
public class GenericMethodExample {

    // Define a generic method
    public static <E> void printArray(E[] inputArray) {
        for (E element : inputArray) {
            System.out.printf("%s ", element);
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4, 5};
        String[] stringArray = {"Hello", "World"};

        System.out.println("Integer array:");
        printArray(intArray); // Compiler infers E is Integer

        System.out.println("String array:");
        printArray(stringArray); // Compiler infers E is String
    }
}

Bounded Type Parameters

Sometimes we want to restrict the types that can be used as type parameters. For example, a method might only accept instances of Number or its subclasses. This can be achieved through bounded type parameters.

  • <T extends UpperBound>: T must be of type UpperBound or its subtype.
java
public class BoundedTypeExample {

    // T must be Number or its subclass (like Integer, Double)
    public static <T extends Number> double getDoubleValue(T number) {
        return number.doubleValue();
    }

    public static void main(String[] args) {
        System.out.println(getDoubleValue(10));       // Integer
        System.out.println(getDoubleValue(3.14));     // Double
        // getDoubleValue("text"); // Compile error! String is not a subclass of Number
    }
}

Wildcards

The wildcard ? represents an unknown type and is commonly used in generic method parameters to increase flexibility.

  1. Upper bounded wildcard (? extends Type): Represents the parameter type is Type or any of its subtypes. Such collections are read-only (cannot add elements except null) because we cannot determine the exact type of the collection.

    java
    // Can accept List<Integer>, List<Double>, etc.
    public static double sumOfList(List<? extends Number> list) {
        double sum = 0.0;
        for (Number n : list) {
            sum += n.doubleValue();
        }
        return sum;
    }
  2. Lower bounded wildcard (? super Type): Represents the parameter type is Type or any of its supertypes. Such collections are write-only (can add instances of Type and its subtypes), but reading only returns Object.

    java
    // Can accept List<Integer>, List<Number>, List<Object>
    public static void addIntegers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
    }
  3. Unbounded wildcard (?): Represents any type. Used when the type doesn't matter, such as List<?>.

Type Erasure

Java generics are implemented through type erasure. This means that after compilation, all generic type information is removed. Box<String> and Box<Integer> at runtime both become the raw Box class, with the type parameter T replaced by its bound (default is Object). The compiler automatically inserts type casting code where necessary to ensure type safety.

Content is for learning and research only.