Java 26-Day Course - Day 17: Generics

Day 17: Generics

Generics allow you to receive types as parameters when defining classes or methods. Think of it as a delivery box that “can hold anything,” but once designated as a “book box,” only books can go in. It checks types at compile time to prevent runtime errors.

Generic Classes

Create classes that handle various types using the type parameter <T>.

// Generic class: T is the type parameter
class Box<T> {
    private T item;

    public void put(T item) {
        this.item = item;
        System.out.println(item.getClass().getSimpleName() + " stored: " + item);
    }

    public T get() {
        return item;
    }

    public boolean isEmpty() {
        return item == null;
    }
}

// Multiple type parameters
class Pair<K, V> {
    private final K key;
    private final V value;

    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;
    }
}

public class GenericClassExample {
    public static void main(String[] args) {
        // String type Box
        Box<String> stringBox = new Box<>();
        stringBox.put("Hello");
        String message = stringBox.get(); // No casting needed!
        System.out.println("Retrieved: " + message);

        // Integer type Box
        Box<Integer> intBox = new Box<>();
        intBox.put(42);
        int number = intBox.get(); // Auto-unboxing
        System.out.println("Retrieved: " + number);

        // Using Pair
        Pair<String, Integer> nameAge = new Pair<>("Alice", 25);
        Pair<String, String> config = new Pair<>("host", "localhost");

        System.out.println(nameAge);
        System.out.println(config);

        // Diamond operator (<>): Java 7+
        // Type on the right side can be omitted (inferred)
        Box<Double> doubleBox = new Box<>();
        doubleBox.put(3.14);
    }
}

Generic Methods

Declare type parameters at the method level to handle various types.

import java.util.Arrays;
import java.util.List;

public class GenericMethodExample {
    // Generic method: declare <T> before the return type
    static <T> void printArray(T[] array) {
        System.out.print("[");
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i]);
            if (i < array.length - 1) System.out.print(", ");
        }
        System.out.println("]");
    }

    // Return the greater of two values
    static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) >= 0 ? a : b;
    }

    // Find a specific value in an array
    static <T> int indexOf(T[] array, T target) {
        for (int i = 0; i < array.length; i++) {
            if (array[i].equals(target)) {
                return i;
            }
        }
        return -1;
    }

    // Convert array to List
    static <T> List<T> arrayToList(T[] array) {
        return Arrays.asList(array);
    }

    public static void main(String[] args) {
        Integer[] intArr = {1, 2, 3, 4, 5};
        String[] strArr = {"Java", "Python", "Go"};
        Double[] dblArr = {1.1, 2.2, 3.3};

        printArray(intArr);
        printArray(strArr);
        printArray(dblArr);

        System.out.println("Greater: " + max(10, 20));
        System.out.println("Greater: " + max("apple", "banana"));

        System.out.println("Python index: " + indexOf(strArr, "Python"));

        List<String> list = arrayToList(strArr);
        System.out.println("List: " + list);
    }
}

Bounded Type Parameters

Restrict type parameters with upper/lower bounds to allow only certain types.

// Upper bound: only Number or its subclasses allowed
class MathBox<T extends Number> {
    private T value;

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

    double doubleValue() {
        return value.doubleValue();
    }

    boolean isPositive() {
        return value.doubleValue() > 0;
    }

    // Sum of two MathBoxes
    <U extends Number> double add(MathBox<U> other) {
        return this.doubleValue() + other.doubleValue();
    }
}

// Multiple bounds: must implement multiple interfaces
class SortableBox<T extends Comparable<T> & java.io.Serializable> {
    private T item;

    SortableBox(T item) {
        this.item = item;
    }

    boolean isGreaterThan(SortableBox<T> other) {
        return this.item.compareTo(other.item) > 0;
    }

    T getItem() {
        return item;
    }
}

public class BoundedTypeExample {
    public static void main(String[] args) {
        MathBox<Integer> intMath = new MathBox<>(42);
        MathBox<Double> dblMath = new MathBox<>(3.14);

        System.out.println("As double: " + intMath.doubleValue());
        System.out.println("Positive? " + intMath.isPositive());
        System.out.println("Sum: " + intMath.add(dblMath));

        // MathBox<String> strMath = new MathBox<>("hello"); // Compile error!

        SortableBox<String> box1 = new SortableBox<>("apple");
        SortableBox<String> box2 = new SortableBox<>("banana");
        System.out.println("box1 > box2? " + box1.isGreaterThan(box2));
    }
}

Wildcards

The ? symbol for flexible use of generic types.

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

public class WildcardExample {
    // Unbounded wildcard: accepts all types (read-only)
    static void printList(List<?> list) {
        for (Object item : list) {
            System.out.print(item + " ");
        }
        System.out.println();
    }

    // Upper bounded wildcard: accepts Number and below (for reading)
    static double sumOfList(List<? extends Number> list) {
        double sum = 0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        return sum;
    }

    // Lower bounded wildcard: accepts Integer and above (for writing)
    static void addNumbers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }

    // PECS principle: Producer Extends, Consumer Super
    // When you take out (produce) data -> extends
    // When you put in (consume) data -> super
    static <T> void copy(List<? extends T> source, List<? super T> dest) {
        for (T item : source) {
            dest.add(item);
        }
    }

    public static void main(String[] args) {
        List<Integer> intList = List.of(1, 2, 3, 4, 5);
        List<Double> dblList = List.of(1.1, 2.2, 3.3);
        List<String> strList = List.of("A", "B", "C");

        printList(intList);
        printList(strList);

        System.out.println("Integer sum: " + sumOfList(intList));
        System.out.println("Double sum: " + sumOfList(dblList));

        List<Number> numberList = new ArrayList<>();
        addNumbers(numberList);
        System.out.println("Added numbers: " + numberList);

        // Copy
        List<Number> dest = new ArrayList<>();
        copy(intList, dest);
        System.out.println("Copy result: " + dest);
    }
}

Today’s Exercises

  1. Generic Stack: Implement a GenericStack<T> class with push(T), pop(), peek(), isEmpty(), and size() methods. Use an ArrayList internally.

  2. Generic Utilities: Implement the following generic methods: swap(T[] arr, int i, int j) - swap two elements in an array, reverse(List<T> list) - reverse a list, filter(List<T> list, Predicate<T> pred) - return only elements matching the condition.

  3. Comparable Usage: Create a Statistics<T> class using the <T extends Comparable<T>> bound that returns the maximum, minimum, and sorted list from a given list.

Was this article helpful?