Generics

Type parameters and generic programming

1. Introduction to Generics

Generics enable types (classes and interfaces) to be parameters when defining classes, interfaces, and methods. They provide stronger type checking at compile time.

Benefits of Generics

  • Compile-time type checking
  • Elimination of casts
  • Generic algorithms
  • Code reusability

Generic Syntax

  • <T> - Type parameter
  • <E> - Element type
  • <K,V> - Key, Value types
  • <?> - Wildcard

Before Generics (Raw Types)

// Without generics - requires casting and runtime checks
List list = new ArrayList();
list.add("Hello");
list.add(123); // Different types mixed!

String str = (String) list.get(0); // Casting required
Integer num = (Integer) list.get(1); // Runtime error possible

With Generics (Type Safe)

// With generics - compile-time type safety
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
// stringList.add(123); // Compile-time error!

String str = stringList.get(0); // No casting needed
// Type safety guaranteed at compile time

2. Type Safety & Generic Classes

Generics provide type safety by enabling you to detect errors at compile time rather than runtime.

Generic Class Example

// Generic class with type parameter T
public class Box<T> {
    private T content;

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

    public T get() {
        return content;
    }

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

// Usage
Box<String> stringBox = new Box<>();
stringBox.set("Hello World");
String value = stringBox.get(); // No casting needed

Box<Integer> intBox = new Box<>();
intBox.set(42);
Integer number = intBox.get();

Generic Methods

public class Utility {
    // Generic method with type parameter
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    // Generic method with return type
    public static <T> T getMiddleElement(T[] array) {
        return array[array.length / 2];
    }

    // Multiple type parameters
    public static <K, V> void printKeyValue(K key, V value) {
        System.out.println(key + " = " + value);
    }
}

// Usage
String[] names = {"Alice", "Bob", "Charlie"};
Utility.swap(names, 0, 2); // Alice and Charlie swapped

Integer[] numbers = {1, 2, 3, 4, 5};
Integer middle = Utility.getMiddleElement(numbers); // Returns 3

3. Wildcards

Wildcards (?) represent unknown types in generic code, providing flexibility when exact types are not known.

Unbounded Wildcards

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

// Can accept any parameterized List
List<String> stringList = Arrays.asList("A", "B", "C");
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);

printList(stringList);  // Works
printList(intList);     // Works
printList(doubleList);  // Works

Upper Bounded Wildcards (extends)

// Upper bounded wildcard - accepts Number and its subtypes
public static double sumNumbers(List<? extends Number> numbers) {
    double sum = 0.0;
    for (Number num : numbers) {
        sum += num.doubleValue();
    }
    return sum;
}

// Usage with different Number subtypes
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5);
List<Float> floats = Arrays.asList(1.0f, 2.0f, 3.0f);

double sum1 = sumNumbers(integers); // Works
double sum2 = sumNumbers(doubles);  // Works
double sum3 = sumNumbers(floats);   // Works

Lower Bounded Wildcards (super)

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

// Usage
List<Integer> intList = new ArrayList<>();
List<Number> numberList = new ArrayList<>();
List<Object> objectList = new ArrayList<>();

addNumbers(intList);    // Works
addNumbers(numberList); // Works
addNumbers(objectList); // Works

PECS Rule

Producer Extends, Consumer Super - Use extends when you only read from a structure (producer), use super when you only write to a structure (consumer).

4. Bounded Parameters

Bounded type parameters restrict the types that can be used as type arguments, enabling access to methods of the bound type.

Single Bound

// Bounded type parameter - must extend Number
public class NumberBox<T extends Number> {
    private T value;

    public NumberBox(T value) {
        this.value = value;
    }

    // Can call Number methods because T extends Number
    public double getDoubleValue() {
        return value.doubleValue();
    }

    public int getIntValue() {
        return value.intValue();
    }

    // Compare numeric values
    public boolean isGreaterThan(NumberBox<? extends Number> other) {
        return this.getDoubleValue() > other.getDoubleValue();
    }
}

// Usage
NumberBox<Integer> intBox = new NumberBox<>(42);
NumberBox<Double> doubleBox = new NumberBox<>(3.14);
boolean result = intBox.isGreaterThan(doubleBox);

Multiple Bounds

// Interface for comparison
interface Drawable {
    void draw();
}

// Class with multiple bounds
public class GraphicElement<T extends Shape & Drawable & Comparable<T>> {
    private T element;

    public GraphicElement(T element) {
        this.element = element;
    }

    public void render() {
        element.draw();        // From Drawable
        System.out.println("Area: " + element.calculateArea()); // From Shape
    }

    public int compare(GraphicElement<T> other) {
        return element.compareTo(other.element); // From Comparable
    }
}

5. Advanced Topics

Explore complex scenarios including generic inheritance, recursive bounds, and type token patterns.

Recursive Type Bounds

// Recursive bound - T must be comparable to itself
public class SortedList<T extends Comparable<T>> {
    private List<T> items = new ArrayList<>();

    public void add(T item) {
        items.add(item);
        Collections.sort(items); // Safe because T implements Comparable<T>
    }

    public T get(int index) {
        return items.get(index);
    }
}

// Usage
SortedList<String> stringList = new SortedList<>();
stringList.add("Charlie");
stringList.add("Alice");
stringList.add("Bob");
// Items are automatically sorted: Alice, Bob, Charlie

Generic Builder Pattern

// Generic builder with fluent interface
public class ApiResponse<T> {
    private final T data;
    private final String message;
    private final int statusCode;

    private ApiResponse(Builder<T> builder) {
        this.data = builder.data;
        this.message = builder.message;
        this.statusCode = builder.statusCode;
    }

    public static <T> Builder<T> builder() {
        return new Builder<>();
    }

    public static class Builder<T> {
        private T data;
        private String message = "Success";
        private int statusCode = 200;

        public Builder<T> data(T data) {
            this.data = data;
            return this;
        }

        public Builder<T> message(String message) {
            this.message = message;
            return this;
        }

        public ApiResponse<T> build() {
            return new ApiResponse<>(this);
        }
    }
}

// Usage
ApiResponse<List<User>> response = ApiResponse.<List<User>>builder()
    .data(userList)
    .message("Users retrieved successfully")
    .build();

6. Best Practices

Good Practices Example

// Good: Clear generic interface
public interface Repository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
    T save(T entity);
    void deleteById(ID id);

    // Using bounded wildcards for flexibility
    List<T> findByExample(T example);
    void saveAll(Collection<? extends T> entities);
}

// Good: Proper use of bounds
public class NumberProcessor<T extends Number & Comparable<T>> {
    public T max(Collection<? extends T> numbers) {
        return numbers.stream()
            .max(T::compareTo)
            .orElse(null);
    }
}

// Avoid: Raw types
List rawList = new ArrayList(); // Don't do this

// Prefer: Parameterized types
List<String> stringList = new ArrayList<>(); // Better

7. Summary

Key Takeaways

  • Type Safety: Compile-time error detection eliminates casting and runtime errors
  • Generic Classes/Methods: Create reusable, type-safe code with type parameters
  • Wildcards: Use ? for unknown types, extends for upper bounds, super for lower bounds
  • PECS Principle: Producer Extends, Consumer Super - remember this for wildcard usage
  • Type Erasure: Generic information removed at runtime for backward compatibility

Type Safety

Compile-time checks, no casting, type parameter constraints

Wildcards

Unbounded (?), upper bounds (extends), lower bounds (super)

Bounded Parameters

Single/multiple bounds, recursive bounds, method access

8. Common Interview Questions

Interview Tips

  • Understand PECS: Know when to use extends vs super
  • Explain Benefits: Type safety, no casting, compile-time checks
  • Code Examples: Be ready to write generic methods
  • Know Limitations: Type erasure, array restrictions