⚡IntermediateGenerics

Generics

Write type-safe, reusable classes and methods using generics to eliminate casting and catch errors at compile time.

Overview

Generics allow you to write classes and methods that work with any type while remaining type-safe. The type parameter is written in angle brackets and acts as a placeholder that is filled in at the call site. Before generics, collections stored Object, requiring a cast on every retrieval and risking ClassCastException at runtime. With generics, the compiler catches type mismatches at compile time. Generic classes are declared with <T> after the class name. Generic methods declare the type parameter before the return type: <T> T method(T input).

Key Points

  • •Generic class: class Box<T> { T value; }
  • •T is a convention for a single type; E for element, K for key, V for value
  • •The compiler erases type parameters at runtime (type erasure); generics are a compile-time feature
  • •Generic methods: <T> T identity(T t) can be called with any type
  • •You cannot use primitives as type parameters: use Integer, not int
  • •You cannot create arrays of generic types: new T[] is not allowed
  • •Generic types allow code reuse without sacrificing type safety
  • •Bounded type parameters restrict which types are allowed (covered in J049)

Examples

Generic class

public class Main {
    // Generic container that holds any type
    static class Box<T> {
        private T value;

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

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

        @Override
        public String toString() { return "Box[" + value + "]"; }
    }

    public static void main(String[] args) {
        Box<String> stringBox = new Box<>("Hello");
        Box<Integer> intBox = new Box<>(42);
        Box<Double> doubleBox = new Box<>(3.14);

        System.out.println(stringBox.getValue()); // Hello
        System.out.println(intBox.getValue());     // 42

        // Compile error prevented:
        // stringBox.setValue(123); // would not compile
    }
}

The type parameter T is replaced at compile time. Box<String> and Box<Integer> are compile-time distinct; mistakes like setting an int into a String box are caught immediately.

Generic class with two type parameters

public class Main {
    static class Pair<A, B> {
        private final A first;
        private final B second;

        Pair(A first, B second) {
            this.first = first;
            this.second = second;
        }

        A getFirst() { return first; }
        B getSecond() { return second; }

        @Override
        public String toString() {
            return "(" + first + ", " + second + ")";
        }
    }

    public static void main(String[] args) {
        Pair<String, Integer> entry = new Pair<>("Alice", 92);
        Pair<Double, Boolean> result = new Pair<>(3.14, true);

        System.out.println(entry);  // (Alice, 92)
        System.out.println(result); // (3.14, true)

        String name = entry.getFirst();   // no cast needed
        int score = entry.getSecond();
        System.out.println(name + " scored " + score);
    }
}

Multiple type parameters are separated by commas. Map.Entry<K,V> in the JDK is a real-world example of this pattern.

Generic method

import java.util.List;
import java.util.ArrayList;

public class Main {
    // Generic method: works with any type T
    static <T> List<T> repeat(T item, int times) {
        List<T> list = new ArrayList<>();
        for (int i = 0; i < times; i++) list.add(item);
        return list;
    }

    static <T> void swap(T[] arr, int i, int j) {
        T temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    public static void main(String[] args) {
        System.out.println(repeat("Java", 3));  // [Java, Java, Java]
        System.out.println(repeat(7, 4));        // [7, 7, 7, 7]

        String[] words = {"hello", "world", "java"};
        swap(words, 0, 2);
        System.out.println(java.util.Arrays.toString(words)); // [java, world, hello]
    }
}

The compiler infers the type parameter from the argument. You can also specify it explicitly: Main.<String>repeat("x", 3).

Common Mistakes

  • Using raw types: Box box = new Box(42) loses all type safety. Always supply the type parameter
  • Trying to use primitives: Box<int> does not compile. Use Box<Integer> and rely on autoboxing
  • Trying to instantiate the type parameter: new T() is not allowed because type information is erased at runtime

Note

Generics only exist at compile time. At runtime, Box<String> and Box<Integer> are both just Box. This is called type erasure and is why you cannot do instanceof checks like value instanceof T.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 4 exercises

Knowledge Check

Question 1 of 4

What is the main benefit of generics over using Object?