← Back to Home

Module 3: Generics

Module Overview

Understand generics in Java, type parameters, and generic methods.

Learning Objectives

Key Concepts

Introduction to Generics

Generics enable you to create classes, interfaces, and methods that operate on types that are specified at compilation time. They provide stronger type checks, eliminate the need for casting, and enable programmers to implement generic algorithms.

Benefits of Generics:

  • Type safety - detect errors at compile time instead of runtime
  • Elimination of casts - no need for explicit type casting
  • Code reusability - write algorithms that work with different types
  • Type-specific operations - perform operations specific to the input type

Example - Without Generics:

// Non-generic collection
public class Box {
    private Object object;
    
    public void set(Object object) {
        this.object = object;
    }
    
    public Object get() {
        return object;
    }
}

// Client code - requires casting and is not type-safe
Box box = new Box();
box.set("Hello World");  // Store a String

// Type casting is required - potential runtime error
String message = (String) box.get();

// This would compile but fail at runtime
Integer wrongType = (Integer) box.get();  // ClassCastException

Example - With Generics:

// Generic collection
public class Box {
    private T t;
    
    public void set(T t) {
        this.t = t;
    }
    
    public T get() {
        return t;
    }
}

// Client code - type-safe and no casting required
Box stringBox = new Box<>();
stringBox.set("Hello World");

// No casting required, and type safety is guaranteed
String message = stringBox.get();

// This won't compile - caught at compile time
// stringBox.set(10);  // Error: incompatible types

Generic Methods

Generic methods allow type parameters to be used in a single method declaration, making it possible to implement algorithms that operate on multiple types while providing type safety.

Example:

public class Util {
    // Generic method to find maximum of two comparable values
    public static > T findMax(T a, T b) {
        if (a.compareTo(b) > 0) {
            return a;
        } else {
            return b;
        }
    }
    
    // Generic method to print array elements
    public static  void printArray(E[] array) {
        for (E element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
}

// Client code
public class TestGenerics {
    public static void main(String[] args) {
        // Using the generic method with Integers
        Integer max = Util.findMax(10, 20);
        System.out.println("Maximum of 10 and 20: " + max);
        
        // Using the generic method with Strings
        String maxString = Util.findMax("apple", "orange");
        System.out.println("Maximum of 'apple' and 'orange': " + maxString);
        
        // Using the generic printArray method
        Integer[] intArray = {1, 2, 3, 4, 5};
        String[] stringArray = {"Hello", "World"};
        
        Util.printArray(intArray);
        Util.printArray(stringArray);
    }
}

Bounded Type Parameters

Bounded type parameters restrict the types that can be used as type arguments in a generic class, interface, or method. This allows you to invoke methods of the bound type without casting.

Types of Bounds:

  • Upper bounded: <T extends UpperBound> - T must be a subtype of UpperBound
  • Multiple bounds: <T extends Class1 & Interface1 & Interface2> - T must implement all specified types
  • Wildcards: <? extends Type> or <? super Type> - for flexible method parameters

Example:

// Upper bounded type parameter
public class NumericCalculator {
    private T number;
    
    public NumericCalculator(T number) {
        this.number = number;
    }
    
    // Can call methods of Number class without casting
    public double getDoubleValue() {
        return number.doubleValue();
    }
    
    public double square() {
        return number.doubleValue() * number.doubleValue();
    }
}

// Example with wildcards
public class WildcardExample {
    // Producer - use "extends" (read-only)
    public static void printList(List list) {
        for (Number n : list) {
            System.out.print(n + " ");
        }
        System.out.println();
    }
    
    // Consumer - use "super" (write-only)
    public static void addNumbers(List list) {
        for (int i = 1; i <= 5; i++) {
            list.add(i);
        }
    }
    
    public static void main(String[] args) {
        // Using bounded type parameter
        NumericCalculator intCalc = new NumericCalculator<>(5);
        System.out.println("Square: " + intCalc.square());
        
        NumericCalculator doubleCalc = new NumericCalculator<>(2.5);
        System.out.println("Double value: " + doubleCalc.getDoubleValue());
        
        // Using wildcards
        List intList = new ArrayList<>();
        List doubleList = new ArrayList<>();
        
        addNumbers(intList);  // Works fine
        // addNumbers(doubleList);  // Won't compile - Double is not a supertype of Integer
        
        printList(intList);     // Works fine - Integer extends Number
        printList(doubleList);  // Works fine - Double extends Number
    }
}

Resources