Skip to content
intermediatePhase 14 · Java Generics

Generics

Master generic classes, methods, interfaces, and wildcards for type-safe code.

1h
2 problems
Topic Progress0%

Generic Classes

Generic Classes

Generic classes allow you to define classes with type parameters, enabling type-safe reuse across different types.

Syntax: class ClassName<T> { ... }

// Simple generic box
public class Box<T> {
    private T content;

    public Box(T content) {
        this.content = content;
    }

    public T getContent() {
        return content;
    }

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

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

// Generic class with multiple 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 + ")";
    }
}

// Usage
import java.util.*;

public class GenericClassDemo {
    public static void main(String[] args) {
        Box<String> stringBox = new Box<>("Hello");
        // stringBox.setContent(42); // COMPILE ERROR - type safety
        String value = stringBox.getContent(); // no casting needed
        System.out.println(stringBox);

        Box<Integer> intBox = new Box<>(42);
        System.out.println(intBox);

        Pair<String, Integer> pair = new Pair<>("age", 25);
        System.out.println(pair); // (age, 25)

        // Generic collections
        List<String> list = new ArrayList<>();
        list.add("Hello");
        // list.add(42); // COMPILE ERROR
        String s = list.get(0); // no casting

        Map<String, Integer> map = new HashMap<>();
        map.put("count", 5);
        int count = map.get("count"); // no casting
    }
}

Key benefits:

  • Compile-time type checking
  • No need for manual casting
  • Cleaner, more readable code
  • Single class works with multiple types

Generic Methods

Generic Methods

Generic methods have their own type parameters, independent of the class. The type parameter is declared before the return type.

Syntax: <T> ReturnType methodName(T param) { ... }

public class GenericMethodDemo {

    // Generic method - prints any array
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    // Generic method - find maximum
    public static <T extends Comparable<T>> T max(T[] array) {
        if (array == null || array.length == 0) return null;
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i].compareTo(max) > 0) {
                max = array[i];
            }
        }
        return max;
    }

    // Generic method - swap elements
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    // Generic method - convert list to array
    public static <T> T[] listToArray(List<T> list, Class<T> clazz) {
        @SuppressWarnings("unchecked")
        T[] array = (T[]) java.lang.reflect.Array.newInstance(clazz, list.size());
        return list.toArray(array);
    }

    // Bounded generic method
    public static <T extends Number> double sum(List<T> list) {
        double total = 0;
        for (T num : list) {
            total += num.doubleValue();
        }
        return total;
    }

    public static void main(String[] args) {
        Integer[] ints = {3, 1, 4, 1, 5, 9};
        String[] strs = {"banana", "apple", "cherry"};

        printArray(ints); // 3 1 4 1 5 9
        printArray(strs); // banana apple cherry

        System.out.println("Max int: " + max(ints)); // 9
        System.out.println("Max str: " + max(strs)); // cherry

        swap(strs, 0, 2);
        printArray(strs); // cherry apple banana

        List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
        System.out.println("Sum: " + sum(nums)); // 15.0

        List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5);
        System.out.println("Sum: " + sum(doubles)); // 7.5
    }
}

Key points:

  • Type parameters are declared before the return type
  • Can have multiple type parameters: <T, U>
  • Type inference: Java can often infer types from arguments
  • Bounded: <T extends Comparable<T>> restricts T

Wildcards

Wildcards

Wildcards (?) provide flexibility in generic types. There are three kinds:

1. Unbounded wildcard: <?>

  • Accepts any type
  • Read-only (cannot add elements except null)

2. Upper bounded: <? extends T>

  • Accepts T or any subclass of T
  • Read-only (cannot add elements except null)
  • Producer: produces T values

3. Lower bounded: <? super T>

  • Accepts T or any superclass of T
  • Can add T or subclasses of T
  • Consumer: consumes T values
import java.util.*;

public class WildcardDemo {

    // Unbounded wildcard - accepts any List
    public static void printList(List<?> list) {
        for (Object elem : list) {
            System.out.print(elem + " ");
        }
        System.out.println();
    }

    // Upper bounded - list of numbers or subclasses
    public static double sumList(List<? extends Number> list) {
        double sum = 0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        return sum;
    }

    // Lower bounded - list of Integer or supertypes
    public static void addNumbers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }

    // PECS: Producer Extends, Consumer Super
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (T item : src) {
            dest.add(item);
        }
    }

    public static void main(String[] args) {
        // Unbounded
        List<String> strings = Arrays.asList("a", "b", "c");
        List<Integer> ints = Arrays.asList(1, 2, 3);
        printList(strings); // a b c
        printList(ints); // 1 2 3

        // Upper bounded
        List<Integer> intList = Arrays.asList(1, 2, 3);
        List<Double> doubleList = Arrays.asList(1.5, 2.5, 3.5);
        System.out.println("Sum ints: " + sumList(intList)); // 6.0
        System.out.println("Sum doubles: " + sumList(doubleList)); // 7.5

        // Lower bounded
        List<Number> numList = new ArrayList<>();
        addNumbers(numList); // can add Integer to List<Number>
        System.out.println("Numbers: " + numList); // [1, 2, 3]

        // PECS example
        List<Object> dest = new ArrayList<>();
        List<Integer> src = Arrays.asList(1, 2, 3);
        copy(dest, src); // Object super Integer, Integer extends Integer
        System.out.println("Copied: " + dest); // [1, 2, 3]
    }
}

PECS Rule:

  • Producer Extends: If a generic type produces values for you to read, use ? extends T
  • Consumer Super: If a generic type consumes values you write to it, use ? super T
  • Exact type: If you both read and write, don't use wildcards

Bounded Types

Bounded Type Parameters

Bounded type parameters restrict the types that can be used as type arguments. Use extends for upper bounds and super for lower bounds.

Syntax: <T extends UpperBound> or <T extends A & B> (multiple bounds)

public class BoundedTypeDemo {

    // Upper bounded: T must be a Number
    public static <T extends Number> double average(T[] array) {
        double sum = 0;
        for (T num : array) {
            sum += num.doubleValue();
        }
        return sum / array.length;
    }

    // Multiple bounds: T must implement Comparable AND Serializable
    public static <T extends Comparable<T> & java.io.Serializable> T findMax(T[] array) {
        if (array == null || array.length == 0) return null;
        T max = array[0];
        for (int i = 1; i < array.length; i++) {
            if (array[i].compareTo(max) > 0) {
                max = array[i];
            }
        }
        return max;
    }

    // Recursive type bound
    public static <T extends Comparable<T>> T maxOfThree(T a, T b, T c) {
        T max = a;
        if (b.compareTo(max) > 0) max = b;
        if (c.compareTo(max) > 0) max = c;
        return max;
    }

    // Practical: generic stack with bounded elements
    public static class BoundedStack<T extends Comparable<T>> {
        private java.util.List<T> elements = new java.util.ArrayList<>();

        public void push(T item) { elements.add(item); }
        public T pop() { return elements.remove(elements.size() - 1); }
        public T peek() { return elements.get(elements.size() - 1); }

        public T findMax() {
            T max = elements.get(0);
            for (T elem : elements) {
                if (elem.compareTo(max) > 0) max = elem;
            }
            return max;
        }
    }

    public static void main(String[] args) {
        Integer[] ints = {1, 2, 3, 4, 5};
        Double[] doubles = {1.5, 2.5, 3.5};
        // String[] strs = {"a", "b", "c"}; // COMPILE ERROR - String not Number

        System.out.println("Avg ints: " + average(ints)); // 3.0
        System.out.println("Avg doubles: " + average(doubles)); // 2.5

        System.out.println("Max of 3: " + maxOfThree(10, 20, 15)); // 20
        System.out.println("Max of 3: " + maxOfThree("a", "c", "b")); // c

        BoundedStack<Integer> stack = new BoundedStack<>();
        stack.push(3); stack.push(1); stack.push(5); stack.push(2);
        System.out.println("Max in stack: " + stack.findMax()); // 5
    }
}

Bounds usage:

  • <T extends Number> — T must be Number or subclass
  • <T extends Comparable<T>> — T must be comparable to itself
  • <T extends A & B> — T must implement both A and B
  • Upper bound provides access to methods of the bound type

Type Erasure

Type Erasure

Java generics are implemented via type erasure. Generic type information is available at compile time but removed at runtime. The JVM treats all generic types as their raw types.

Implications:

  • You cannot use instanceof with generic types
  • You cannot create generic arrays
  • You cannot create new instances of type parameters
  • All generic types become their bound (or Object) at runtime
import java.util.*;

public class TypeErasureDemo {

    // At runtime, List<String> and List<Integer> are both just List
    public static void demonstrateErasure() {
        List<String> strings = new ArrayList<>();
        List<Integer> ints = new ArrayList<>();

        System.out.println("String list class: " + strings.getClass());
        System.out.println("Integer list class: " + ints.getClass());
        System.out.println("Same class? " + (strings.getClass() == ints.getClass())); // true!
    }

    // Cannot use instanceof with generic types
    public static <T> boolean checkType(Object obj) {
        // return obj instanceof T; // COMPILE ERROR
        return obj.getClass().equals(Object.class); // workaround
    }

    // Cannot create generic arrays
    public static <T> T[] badCreateArray() {
        // return new T[10]; // COMPILE ERROR
        @SuppressWarnings("unchecked")
        T[] array = (T[]) new Object[10]; // workaround
        return array;
    }

    // Bridge methods for overriding
    static class Parent<T> {
        private T value;
        public T getValue() { return value; }
        public void setValue(T value) { this.value = value; }
    }

    static class Child extends Parent<String> {
        @Override
        public String getValue() { return super.getValue(); }
        @Override
        public void setValue(String value) { super.setValue(value); }
        // Compiler generates bridge method:
        // public void setValue(Object value) { setValue((String) value); }
    }

    // Erasure in practice
    public static <T extends Comparable<T>> void sort(List<T> list) {
        // At runtime, this is just sort(List list)
        // T becomes Comparable
    }

    public static void main(String[] args) {
        demonstrateErasure();

        List<String> strList = new ArrayList<>();
        List<Integer> intList = new ArrayList<>();
        System.out.println("Same runtime type: " + (strList.getClass() == intList.getClass()));

        // Workaround for instanceof with generics
        Object obj = "Hello";
        if (obj instanceof String) {
            System.out.println("Is String");
        }

        // Workaround for creating generic arrays
        String[] strArray = createArray(String.class, 5);
        System.out.println("Array length: " + strArray.length);
    }

    @SuppressWarnings("unchecked")
    public static <T> T[] createArray(Class<T> clazz, int size) {
        return (T[]) java.lang.reflect.Array.newInstance(clazz, size);
    }
}

Workarounds for type erasure limitations:

  • Use Class<T> parameter to work with types at runtime
  • Use @SuppressWarnings("unchecked") when you're certain of type safety
  • Use Array.newInstance() for creating generic arrays
  • Pass Class<T> as a method parameter for type reification

Practice Problems

0/2solved
Generic Stack Implementation

Implement a generic Stack<T> class using an ArrayList internally. It should provide push, pop, peek, isEmpty, and size methods.

Solution
import java.util.*;

public class MyStack<T> {
    private ArrayList<T> elements;

    public MyStack() {
        elements = new ArrayList<>();
    }

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

    public T pop() {
        if (isEmpty()) throw new RuntimeException("Stack is empty");
        return elements.remove(elements.size() - 1);
    }

    public T peek() {
        if (isEmpty()) throw new RuntimeException("Stack is empty");
        return elements.get(elements.size() - 1);
    }

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

    public int size() {
        return elements.size();
    }
}
Generic Pair Utility

Create a utility class with a generic method `swap(Pair<T>)` that takes a Pair<T> and returns a new Pair<T> with elements swapped. Also create a `firstOrDefault` method with bounded wildcards.

Solution
public class PairUtils {
    public static <T> Pair<T> swap(Pair<T> pair) {
        return new Pair<>(pair.getSecond(), pair.getFirst());
    }
}

Quiz

1. What does the `?` symbol mean in a generic type like `List<?>`?

Question 1 options

2. What is the PECS rule?

Question 2 options

3. What happens to generic types at runtime due to type erasure?

Question 3 options

4. Can you create an instance of a generic type parameter at runtime?

Question 4 options

Flashcards

Question

What is the difference between `<?>`, `<? extends T>`, and `<? super T>`?

Answer

<?> accepts any type (read-only). <? extends T> accepts T or subtypes (producer, read-only). <? super T> accepts T or supertypes (consumer, can write T).

Question

What is type erasure in Java generics?

Answer

Type erasure removes generic type information at compile time. At runtime, all generic types become their raw types (bound or Object). This means you cannot use instanceof with generics.

Question

How do you declare a bounded type parameter?

Answer

Use `T extends BoundType` to restrict T to the bound type or its subtypes. For multiple bounds: `T extends A & B`. The bound provides access to the bound type's methods.

Question

What is the PECS principle?

Answer

Producer Extends, Consumer Super. Use `? extends T` when the generic produces values you read. Use `? super T` when it consumes values you write. This is the key to designing flexible generic APIs.

Question

What is Java Generics?

Answer

Java Generics is a key concept in Java programming.

Revision Notes

Key Takeaways

  • 1.Generics provide compile-time type safety and eliminate casting
  • 2.Wildcards provide flexibility: `?` (any), `extends` (read), `super` (write)
  • 3.Type erasure removes generic info at runtime — no new T(), no instanceof
  • 4.PECS: Producer Extends, Consumer Super

Interview Tips

  • Explain type erasure and its implications for runtime behavior
  • Demonstrate understanding of PECS with practical examples
  • Know why you cannot create new T() or use instanceof with generics
  • Discuss bounded type parameters and when to use multiple bounds

Cheat Sheet

Java Generics Cheat Sheet

Generic Class

class Box<T> {
    private T content;
}

Generic Method

<T> T method(T param) { ... }
<T extends Number> double sum(List<T> list) { ... }

Wildcards

  • <?> — any type (read-only)
  • <? extends T> — T or subtypes (producer)
  • <? super T> — T or supertypes (consumer)

Bounds

  • <T extends Number> — upper bound
  • <T extends A & B> — multiple bounds

Type Erasure

  • Generic types erased at compile time
  • Cannot use instanceof with generics
  • Cannot create new T()
  • Cannot create generic arrays

PECS

Producer Extends, Consumer Super