Java Programming Generics Tutorial
Type Safety

Java Generics - Complete Tutorial

Master Java Generics: Learn type parameters, generic classes and methods, bounded types, wildcards, type erasure with practical examples and best practices for type-safe code.

Type Safety

Compile-time checking

Code Reuse

Single implementation

Bounded Types

Restrict type parameters

Wildcards

Flexible typing

1. Introduction to Java Generics

Generics in Java are a compile-time type safety feature that allows you to write parameterized types. Introduced in Java 5, generics enable types (classes and interfaces) to be parameters when defining classes, interfaces, and methods.

Why Use Generics?
  • Type Safety: Catch type errors at compile-time
  • Code Reuse: Write once, use with different types
  • No Casting Needed: Eliminate explicit type casting
  • Better Readability: Clear type intentions in code
  • Algorithm Reuse: Implement algorithms independently of data types
  • Collection Framework: Foundation of type-safe collections
Key Concepts
  • Type Parameters: <T>, <E>, <K,V>
  • Generic Classes: Classes with type parameters
  • Generic Methods: Methods with their own type parameters
  • Bounded Types: <T extends Number>
  • Wildcards: ?, ? extends T, ? super T
  • Type Erasure: How generics work at runtime
Before vs After Generics
Without Generics
// Pre-Java 5 - Raw types
List list = new ArrayList();
list.add("Hello");
list.add(123);  // No compile error!
String s = (String) list.get(0);  // Cast needed
Integer i = (Integer) list.get(1);  // Runtime ClassCastException!
With Generics
// Java 5+ - Type safety
List list = new ArrayList<>();
list.add("Hello");
list.add(123);  // Compile error!
String s = list.get(0);  // No cast needed
// Integer i = list.get(1);  // Won't compile

2. Generic Classes

A generic class is a class that can work with different types. The type is specified as a parameter when the class is instantiated.

GenericClassExample.java
// Simple generic class with single type parameter
public class Box {
    private T content;
    
    public Box() {
        this.content = null;
    }
    
    public Box(T content) {
        this.content = content;
    }
    
    public void setContent(T content) {
        this.content = content;
    }
    
    public T getContent() {
        return content;
    }
    
    @Override
    public String toString() {
        return "Box containing: " + content;
    }
}

// Generic class with multiple type parameters
public class Pair {
    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; }
    
    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
    
    @Override
    public String toString() {
        return "Pair[" + key + " => " + value + "]";
    }
}

// Test generic classes
public class GenericClassExample {
    public static void main(String[] args) {
        // Box with String
        Box stringBox = new Box<>("Hello Generics!");
        System.out.println(stringBox);
        
        // Box with Integer
        Box intBox = new Box<>(42);
        System.out.println(intBox);
        
        // Box with custom object
        Box personBox = new Box<>(new Person("Alice", 30));
        System.out.println(personBox);
        
        // Pair example
        Pair nameAge = new Pair<>("Bob", 25);
        System.out.println(nameAge);
        
        Pair idName = new Pair<>(101, "Charlie");
        System.out.println(idName);
        
        // Generic class inheritance
        NumberBox doubleBox = new NumberBox<>(3.14);
        System.out.println("Double box: " + doubleBox);
    }
}

// Generic class extending another class
class NumberBox extends Box {
    public NumberBox(T content) {
        super(content);
    }
    
    public double square() {
        return getContent().doubleValue() * getContent().doubleValue();
    }
}

// Simple Person class for example
class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}
Type Parameter Common Meaning Example Where Used
T Type Box<T> General purpose
E Element List<E> Collections
K Key Map<K,V> Maps
V Value Map<K,V> Maps
N Number Calculator<N> Numerics

3. Generic Methods

Generic methods are methods that introduce their own type parameters. This is useful when you want to create a method that can work with different types independently of the class's type parameters.

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

public class GenericMethodExample {
    
    // Simple generic method
    public static  void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
    
    // Generic method returning a value
    public static  T getMiddle(T[] array) {
        if (array == null || array.length == 0) {
            return null;
        }
        return array[array.length / 2];
    }
    
    // Generic method with multiple type parameters
    public static  void printPair(K key, V value) {
        System.out.println(key + " => " + value);
    }
    
    // Generic method in non-generic class
    public static > T max(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }
    
    // Generic method with wildcard parameter
    public static void printList(List list) {
        for (Object elem : list) {
            System.out.print(elem + " ");
        }
        System.out.println();
    }
    
    // Generic method using type inference
    public static  T[] swap(T[] array, int i, int j) {
        if (i < 0 || i >= array.length || j < 0 || j >= array.length) {
            throw new IllegalArgumentException("Invalid indices");
        }
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
        return array;
    }
    
    public static void main(String[] args) {
        // Test printArray with different types
        Integer[] intArray = {1, 2, 3, 4, 5};
        String[] strArray = {"Hello", "World", "Java"};
        Double[] doubleArray = {1.1, 2.2, 3.3};
        
        System.out.print("Integer array: ");
        printArray(intArray);
        
        System.out.print("String array: ");
        printArray(strArray);
        
        System.out.print("Double array: ");
        printArray(doubleArray);
        
        // Test getMiddle
        System.out.println("Middle of int array: " + getMiddle(intArray));
        System.out.println("Middle of str array: " + getMiddle(strArray));
        
        // Test printPair
        printPair("Name", "Alice");
        printPair("Age", 25);
        printPair("Salary", 50000.50);
        
        // Test max method
        System.out.println("Max of 10 and 20: " + max(10, 20));
        System.out.println("Max of 'A' and 'Z': " + max('A', 'Z'));
        System.out.println("Max of 3.14 and 2.71: " + max(3.14, 2.71));
        
        // Test swap method
        Integer[] numbers = {1, 2, 3, 4, 5};
        System.out.println("Original: " + Arrays.toString(numbers));
        swap(numbers, 0, 4);
        System.out.println("After swap: " + Arrays.toString(numbers));
        
        // Type inference in action
        String[] fruits = {"Apple", "Banana", "Cherry"};
        String[] swapped = swap(fruits, 0, 2);  // Type inferred
        System.out.println("Swapped fruits: " + Arrays.toString(swapped));
        
        // Using generic method from utility class
        System.out.println("Is empty string? " + 
            GenericUtils.isEmpty(""));
        System.out.println("Is empty list? " + 
            GenericUtils.isEmpty(Arrays.asList()));
    }
}

// Utility class with generic methods
class GenericUtils {
    
    // Generic method to check if object is null
    public static  boolean isNull(T obj) {
        return obj == null;
    }
    
    // Generic method for empty check
    public static  boolean isEmpty(T obj) {
        if (obj == null) return true;
        if (obj instanceof String) return ((String) obj).isEmpty();
        if (obj instanceof java.util.Collection) 
            return ((java.util.Collection) obj).isEmpty();
        return false;
    }
    
    // Generic method to create array of given type
    @SuppressWarnings("unchecked")
    public static  T[] createArray(Class type, int size) {
        return (T[]) java.lang.reflect.Array.newInstance(type, size);
    }
}
Generic Method Best Practices:
  • Place type parameter before return type: <T> returnType
  • Use descriptive type parameter names when possible
  • Keep generic methods small and focused
  • Document type constraints in method javadoc
  • Consider using bounded type parameters for operations
  • Test generic methods with different type arguments

4. Bounded Type Parameters

Bounded type parameters restrict the types that can be used as type arguments. You can specify that a type parameter must be a subtype of a particular class or implement certain interfaces.

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

// Upper bound - T must be Number or its subclass
class NumberProcessor {
    private T number;
    
    public NumberProcessor(T number) {
        this.number = number;
    }
    
    public double getSquare() {
        return number.doubleValue() * number.doubleValue();
    }
    
    public double getSquareRoot() {
        return Math.sqrt(number.doubleValue());
    }
    
    public boolean isGreaterThan(T other) {
        return number.doubleValue() > other.doubleValue();
    }
}

// Multiple bounds - T must implement both Comparable and Serializable
class SortedContainer & java.io.Serializable> {
    private List items = new ArrayList<>();
    
    public void add(T item) {
        items.add(item);
        items.sort(null);  // Uses natural ordering
    }
    
    public T getMax() {
        if (items.isEmpty()) return null;
        return items.get(items.size() - 1);
    }
    
    public T getMin() {
        if (items.isEmpty()) return null;
        return items.get(0);
    }
}

// Interface bound
interface Drawable {
    void draw();
}

class Circle implements Drawable {
    private double radius;
    
    public Circle(double radius) {
        this.radius = radius;
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing circle with radius: " + radius);
    }
}

class Rectangle implements Drawable {
    private double width, height;
    
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing rectangle: " + width + "x" + height);
    }
}

class DrawingBoard {
    private List shapes = new ArrayList<>();
    
    public void addShape(T shape) {
        shapes.add(shape);
    }
    
    public void drawAll() {
        for (T shape : shapes) {
            shape.draw();
        }
    }
}

// Generic method with bounded type parameter
class MathUtils {
    public static  double sum(List numbers) {
        double total = 0.0;
        for (T num : numbers) {
            total += num.doubleValue();
        }
        return total;
    }
    
    public static > 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;
    }
    
    // Multiple bounds in method
    public static > T clamp(T value, T min, T max) {
        if (value.compareTo(min) < 0) return min;
        if (value.compareTo(max) > 0) return max;
        return value;
    }
}

public class BoundedTypesExample {
    public static void main(String[] args) {
        // Test NumberProcessor with different Number types
        NumberProcessor intProcessor = new NumberProcessor<>(5);
        System.out.println("Integer square: " + intProcessor.getSquare());
        System.out.println("Integer sqrt: " + intProcessor.getSquareRoot());
        
        NumberProcessor doubleProcessor = new NumberProcessor<>(2.5);
        System.out.println("Double square: " + doubleProcessor.getSquare());
        
        // This won't compile - String is not a Number
        // NumberProcessor stringProcessor = new NumberProcessor<>("test");
        
        // Test SortedContainer
        SortedContainer stringContainer = new SortedContainer<>();
        stringContainer.add("Zebra");
        stringContainer.add("Apple");
        stringContainer.add("Banana");
        System.out.println("Max string: " + stringContainer.getMax());
        System.out.println("Min string: " + stringContainer.getMin());
        
        // Test DrawingBoard
        DrawingBoard board = new DrawingBoard<>();
        board.addShape(new Circle(5.0));
        board.addShape(new Rectangle(10.0, 20.0));
        System.out.println("\nDrawing all shapes:");
        board.drawAll();
        
        // Test MathUtils generic methods
        List integers = List.of(1, 2, 3, 4, 5);
        System.out.println("Sum of integers: " + MathUtils.sum(integers));
        
        List doubles = List.of(1.1, 2.2, 3.3);
        System.out.println("Sum of doubles: " + MathUtils.sum(doubles));
        
        // Test findMax
        Integer[] intArray = {3, 1, 4, 1, 5};
        System.out.println("Max integer: " + MathUtils.findMax(intArray));
        
        String[] strArray = {"Apple", "Zebra", "Banana"};
        System.out.println("Max string: " + MathUtils.findMax(strArray));
        
        // Test clamp
        Integer clamped = MathUtils.clamp(15, 10, 20);
        System.out.println("Clamped 15 between 10-20: " + clamped);
        
        Double clampedDouble = MathUtils.clamp(25.5, 10.0, 20.0);
        System.out.println("Clamped 25.5 between 10.0-20.0: " + clampedDouble);
    }
}
Bound Type Syntax Description Example
Upper Bound <T extends Class> T must be subclass of Class <T extends Number>
Interface Bound <T extends Interface> T must implement Interface <T extends Comparable>
Multiple Bounds <T extends A & B> T must extend A and implement B <T extends Number & Comparable>
Wildcard Bound <? extends Class> Unknown type extending Class List<? extends Number>

5. Wildcards in Generics

Wildcards (?) represent unknown types in generics. They provide flexibility when you don't know or don't care about the specific type parameter.

UpperBoundedWildcard.java
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

public class UpperBoundedWildcard {
    
    // Upper bounded wildcard - accepts List of any type that extends Number
    public static double sumOfList(List list) {
        double sum = 0.0;
        for (Number num : list) {
            sum += num.doubleValue();
        }
        return sum;
    }
    
    // Method that works with any List of Comparable objects
    public static > T maxInRange(
            List list, int begin, int end) {
        if (list == null || list.isEmpty() || begin >= end) {
            return null;
        }
        
        T max = list.get(begin);
        for (int i = begin + 1; i < end && i < list.size(); i++) {
            T current = list.get(i);
            if (current.compareTo(max) > 0) {
                max = current;
            }
        }
        return max;
    }
    
    // Process list of numbers
    public static void processNumbers(List numbers) {
        System.out.println("Processing numbers:");
        for (Number num : numbers) {
            System.out.println("  Value: " + num + ", Type: " + 
                             num.getClass().getSimpleName());
        }
    }
    
    public static void main(String[] args) {
        // Test with different List types
        List integers = Arrays.asList(1, 2, 3, 4, 5);
        List doubles = Arrays.asList(1.1, 2.2, 3.3, 4.4);
        List floats = Arrays.asList(1.5f, 2.5f, 3.5f);
        
        System.out.println("Sum of integers: " + sumOfList(integers));
        System.out.println("Sum of doubles: " + sumOfList(doubles));
        System.out.println("Sum of floats: " + sumOfList(floats));
        
        // Can't add to upper bounded wildcard list (except null)
        List numbers = integers;
        // numbers.add(10);  // Compile error!
        numbers.add(null);  // Only null is allowed
        
        // Process different number lists
        processNumbers(integers);
        processNumbers(doubles);
        
        // Test maxInRange
        List scores = Arrays.asList(85, 92, 78, 95, 88);
        Integer maxScore = maxInRange(scores, 0, 3);
        System.out.println("Max score in range: " + maxScore);
        
        List names = Arrays.asList("Alice", "Bob", "Charlie", "David");
        String maxName = maxInRange(names, 1, 4);
        System.out.println("Max name in range: " + maxName);
    }
}
LowerBoundedWildcard.java
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;

public class LowerBoundedWildcard {
    
    // Lower bounded wildcard - accepts List of Integer or superclasses
    public static void addIntegers(List list) {
        for (int i = 1; i <= 5; i++) {
            list.add(i);  // Can add Integer
        }
    }
    
    // Copy from source to destination
    public static  void copy(List dest, List src) {
        for (T element : src) {
            dest.add(element);
        }
    }
    
    // Add numbers to a collection
    public static void addNumbers(List list) {
        list.add(10);        // Integer
        list.add(3.14);      // Double
        list.add(2.5f);      // Float
        list.add(100L);      // Long
    }
    
    // Process consumer (can add items)
    public static void processConsumer(List list) {
        list.add("Hello");
        list.add("World");
        // String item = list.get(0);  // Can't safely get - returns Object
        Object item = list.get(0);  // Must use Object
    }
    
    public static void main(String[] args) {
        // Test lower bounded wildcards
        
        // Can add to List
        List numbers = new ArrayList<>();
        addIntegers(numbers);
        System.out.println("Numbers after adding integers: " + numbers);
        
        // Can add to List
        List objects = new ArrayList<>();
        addIntegers(objects);
        System.out.println("Objects after adding integers: " + objects);
        
        // Can add various numbers
        List numberList = new ArrayList<>();
        addNumbers(numberList);
        System.out.println("Various numbers added: " + numberList);
        
        // Test copy method
        List source = Arrays.asList(1, 2, 3, 4, 5);
        List destination = new ArrayList<>();
        
        copy(destination, source);
        System.out.println("Copied to destination: " + destination);
        
        // Copy Strings to Object list
        List strings = Arrays.asList("A", "B", "C");
        List objList = new ArrayList<>();
        copy(objList, strings);
        System.out.println("Copied strings: " + objList);
        
        // PECS principle demonstration
        List producer = Arrays.asList(1, 2, 3);
        List consumer = new ArrayList();
        
        // Producer - can only get items
        Number first = producer.get(0);
        // producer.add(4);  // Compile error - can't add to producer
        
        // Consumer - can add items
        consumer.add(10);
        consumer.add(3.14);
        // Number num = consumer.get(0);  // Can't - returns Object
        Object obj = consumer.get(0);
        
        System.out.println("\nPECS Principle:");
        System.out.println("Producer (extends): Can only GET items");
        System.out.println("Consumer (super): Can only ADD items");
    }
}

// Unbounded wildcard example
class UnboundedWildcardExample {
    
    // Unbounded wildcard - accepts List of any type
    public static void printList(List list) {
        for (Object obj : list) {
            System.out.print(obj + " ");
        }
        System.out.println();
    }
    
    // Check if list is empty (doesn't care about type)
    public static boolean isEmpty(List list) {
        return list == null || list.isEmpty();
    }
    
    // Get size of any list
    public static int getSize(List list) {
        return list.size();
    }
    
    public static void main(String[] args) {
        List strings = Arrays.asList("A", "B", "C");
        List integers = Arrays.asList(1, 2, 3);
        List doubles = Arrays.asList(1.1, 2.2, 3.3);
        
        System.out.println("Printing different lists:");
        printList(strings);
        printList(integers);
        printList(doubles);
        
        // Can't add to unbounded wildcard (except null)
        List unknownList = strings;
        // unknownList.add("D");  // Compile error!
        unknownList.add(null);  // Only null is allowed
        
        System.out.println("Is strings empty? " + isEmpty(strings));
        System.out.println("Size of integers: " + getSize(integers));
    }
}
                    
                
            
            
            
PECS Principle (Producer-Extends, Consumer-Super):
  • Producer (extends): When you only GET items from a structure
  • Consumer (super): When you only PUT items into a structure
  • Both (no wildcard): When you need to both get and put
  • Mnemonic: "Get and Put Principle" or "PECS"
  • Example: copy(List<? super T> dest, List<? extends T> src)

6. Type Erasure and Bridge Methods

Type erasure is the process by which the Java compiler removes all type parameters and replaces them with their bounds or Object. This ensures backward compatibility with pre-generics code.

TypeErasureExample.java
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

// Generic class that will undergo type erasure
public class ErasureDemo {
    private T value;
    
    public ErasureDemo(T value) {
        this.value = value;
    }
    
    public T getValue() {
        return value;
    }
    
    public void setValue(T value) {
        this.value = value;
    }
    
    // Generic method
    public  E process(E input) {
        System.out.println("Processing: " + input);
        return input;
    }
}

// Class demonstrating bridge methods
class ComparablePair> implements Comparable> {
    private T first;
    private T second;
    
    public ComparablePair(T first, T second) {
        this.first = first;
        this.second = second;
    }
    
    @Override
    public int compareTo(ComparablePair other) {
        int firstCompare = this.first.compareTo(other.first);
        if (firstCompare != 0) {
            return firstCompare;
        }
        return this.second.compareTo(other.second);
    }
    
    // The compiler adds a bridge method:
    // public int compareTo(Object other) {
    //     return compareTo((ComparablePair)other);
    // }
}

public class TypeErasureExample {
    public static void main(String[] args) throws Exception {
        System.out.println("=== Type Erasure Demonstration ===");
        
        // Create generic instances
        ErasureDemo stringDemo = new ErasureDemo<>("Hello");
        ErasureDemo intDemo = new ErasureDemo<>(42);
        
        // Check runtime types - both are just ErasureDemo
        System.out.println("stringDemo class: " + stringDemo.getClass());
        System.out.println("intDemo class: " + intDemo.getClass());
        System.out.println("Same class? " + 
            (stringDemo.getClass() == intDemo.getClass()));
        
        // Reflection shows type erasure
        System.out.println("\n=== Reflection on ErasureDemo ===");
        Class clazz = ErasureDemo.class;
        System.out.println("Class name: " + clazz.getName());
        
        // List all methods
        System.out.println("\nMethods in ErasureDemo:");
        for (Method method : clazz.getDeclaredMethods()) {
            System.out.println("  " + method);
            if (method.isSynthetic()) {
                System.out.println("    ^ This is a synthetic/bridge method");
            }
        }
        
        // Raw type usage (for backward compatibility)
        System.out.println("\n=== Raw Types ===");
        ErasureDemo rawDemo = new ErasureDemo("Test");
        rawDemo.setValue("String");  // OK
        rawDemo.setValue(123);       // Also OK - no type checking!
        
        // Unsafe cast warning
        @SuppressWarnings("unchecked")
        String value = (String) rawDemo.getValue();  // Potential ClassCastException
        
        // Type erasure in arrays
        System.out.println("\n=== Arrays and Generics ===");
        // Generic array creation is not allowed
        // ErasureDemo[] array = new ErasureDemo[10];  // Error
        
        // But you can create raw array
        ErasureDemo[] safeArray = new ErasureDemo[10];  // OK
        safeArray[0] = new ErasureDemo<>("Test");
        
        // Or use ArrayList instead
        List> list = new ArrayList<>();
        list.add(new ErasureDemo<>("Item1"));
        
        // Bridge methods demonstration
        System.out.println("\n=== Bridge Methods ===");
        ComparablePair pair1 = new ComparablePair<>("A", "B");
        ComparablePair pair2 = new ComparablePair<>("A", "C");
        
        System.out.println("Compare result: " + pair1.compareTo(pair2));
        
        // Check for bridge methods
        Class pairClass = ComparablePair.class;
        System.out.println("\nMethods in ComparablePair:");
        for (Method method : pairClass.getDeclaredMethods()) {
            System.out.println("  " + method);
            if (method.isBridge()) {
                System.out.println("    ^ This is a BRIDGE method");
            }
        }
        
        // Type erasure limitations
        System.out.println("\n=== Type Erasure Limitations ===");
        
        // Cannot use instanceof with generic types
        Object obj = "Test";
        // if (obj instanceof List) { }  // Compile error
        
        // Workaround
        if (obj instanceof List) {
            @SuppressWarnings("unchecked")
            List list2 = (List) obj;  // Unchecked cast
        }
        
        // Cannot create arrays of generic types
        // T[] array = new T[10];  // Compile error
        
        // Workaround using reflection
        String[] strArray = createArray(String.class, 5);
        System.out.println("Created array of type: " + strArray.getClass());
        
        // Cannot overload methods with same erasure
        // void process(List list) { }
        // void process(List list) { }  // Compile error - same erasure
    }
    
    // Helper method to create generic array using reflection
    @SuppressWarnings("unchecked")
    public static  T[] createArray(Class type, int size) {
        return (T[]) java.lang.reflect.Array.newInstance(type, size);
    }
    
    // Demonstration of overloading issue
    public void processList(List list) {
        System.out.println("Processing String list");
    }
    
    // This won't compile - same erasure as above
    // public void processList(List list) {
    //     System.out.println("Processing Integer list");
    // }
}
Type Erasure Limitations
  • No instanceof with generic types
  • Cannot create arrays of generic types
  • Cannot overload methods with same erasure
  • Type information lost at runtime
  • Cannot catch generic exceptions
  • Cannot use primitives as type arguments
Workarounds
  • Use Class<T> parameter for type info
  • Use ArrayList instead of arrays
  • Use different method names for overloads
  • Store type token for runtime type checking
  • Use wrapper classes for primitives
  • Consider using reflection when necessary

7. Practical Generics Examples

Example 1: Generic Repository Pattern

import java.util.*;
import java.util.function.Predicate;

// Generic Repository interface
interface Repository {
    T save(T entity);
    Optional findById(ID id);
    List findAll();
    List findBy(Predicate predicate);
    void deleteById(ID id);
    long count();
}

// In-memory implementation
class InMemoryRepository implements Repository {
    private Map storage = new HashMap<>();
    private IdGenerator idGenerator;
    
    public InMemoryRepository(IdGenerator idGenerator) {
        this.idGenerator = idGenerator;
    }
    
    @Override
    public T save(T entity) {
        ID id = idGenerator.generateId(entity);
        storage.put(id, entity);
        return entity;
    }
    
    @Override
    public Optional findById(ID id) {
        return Optional.ofNullable(storage.get(id));
    }
    
    @Override
    public List findAll() {
        return new ArrayList<>(storage.values());
    }
    
    @Override
    public List findBy(Predicate predicate) {
        return storage.values().stream()
                     .filter(predicate)
                     .toList();
    }
    
    @Override
    public void deleteById(ID id) {
        storage.remove(id);
    }
    
    @Override
    public long count() {
        return storage.size();
    }
}

// ID generator interface
interface IdGenerator {
    ID generateId(Object entity);
}

// Integer ID generator
class IntegerIdGenerator implements IdGenerator {
    private int counter = 0;
    
    @Override
    public Integer generateId(Object entity) {
        return ++counter;
    }
}

// UUID generator
class UUIDGenerator implements IdGenerator {
    @Override
    public UUID generateId(Object entity) {
        return UUID.randomUUID();
    }
}

// Domain entities
class Product {
    private String name;
    private double price;
    private String category;
    
    public Product(String name, double price, String category) {
        this.name = name;
        this.price = price;
        this.category = category;
    }
    
    // Getters and setters
    public String getName() { return name; }
    public double getPrice() { return price; }
    public String getCategory() { return category; }
    
    @Override
    public String toString() {
        return String.format("Product{name='%s', price=%.2f, category='%s'}", 
                           name, price, category);
    }
}

class Customer {
    private String name;
    private String email;
    private int age;
    
    public Customer(String name, String email, int age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }
    
    public String getName() { return name; }
    public String getEmail() { return email; }
    public int getAge() { return age; }
    
    @Override
    public String toString() {
        return String.format("Customer{name='%s', email='%s', age=%d}", 
                           name, email, age);
    }
}

public class RepositoryPatternExample {
    public static void main(String[] args) {
        System.out.println("=== Generic Repository Pattern Example ===\n");
        
        // Create repositories
        Repository productRepo = 
            new InMemoryRepository<>(new IntegerIdGenerator());
        
        Repository customerRepo = 
            new InMemoryRepository<>(new UUIDGenerator());
        
        // Add products
        Product laptop = productRepo.save(new Product("Laptop", 999.99, "Electronics"));
        Product phone = productRepo.save(new Product("Phone", 699.99, "Electronics"));
        Product book = productRepo.save(new Product("Java Book", 49.99, "Books"));
        
        // Add customers
        Customer alice = customerRepo.save(new Customer("Alice", "alice@email.com", 30));
        Customer bob = customerRepo.save(new Customer("Bob", "bob@email.com", 25));
        
        // Find operations
        System.out.println("All Products:");
        productRepo.findAll().forEach(System.out::println);
        
        System.out.println("\nAll Customers:");
        customerRepo.findAll().forEach(System.out::println);
        
        // Find by predicate
        System.out.println("\nExpensive Products (price > 500):");
        productRepo.findBy(p -> p.getPrice() > 500)
                   .forEach(System.out::println);
        
        System.out.println("\nElectronics Products:");
        productRepo.findBy(p -> "Electronics".equals(p.getCategory()))
                   .forEach(System.out::println);
        
        // Find by ID
        System.out.println("\nFind product by ID 1:");
        productRepo.findById(1).ifPresentOrElse(
            System.out::println,
            () -> System.out.println("Not found")
        );
        
        // Count
        System.out.println("\nTotal products: " + productRepo.count());
        System.out.println("Total customers: " + customerRepo.count());
        
        // Delete
        productRepo.deleteById(2);
        System.out.println("\nAfter deleting product ID 2:");
        System.out.println("Total products: " + productRepo.count());
        
        // Generic utility method
        System.out.println("\n=== Generic Utility Methods ===");
        List products = productRepo.findAll();
        List customers = customerRepo.findAll();
        
        System.out.println("First product: " + getFirst(products));
        System.out.println("First customer: " + getFirst(customers));
        
        // Convert to map
        Map productMap = toMap(
            products,
            p -> products.indexOf(p) + 1
        );
        System.out.println("\nProduct Map: " + productMap);
    }
    
    // Generic utility methods
    public static  T getFirst(List list) {
        return list.isEmpty() ? null : list.get(0);
    }
    
    public static  Map toMap(List list, Function keyExtractor) {
        Map map = new HashMap<>();
        for (T item : list) {
            map.put(keyExtractor.apply(item), item);
        }
        return map;
    }
}

// Functional interface for completeness
interface Function {
    R apply(T t);
}

Example 2: Generic Math Calculator

import java.util.*;
import java.util.function.BinaryOperator;

// Generic calculator interface
interface Calculator {
    T add(T a, T b);
    T subtract(T a, T b);
    T multiply(T a, T b);
    T divide(T a, T b);
    T power(T base, int exponent);
    T squareRoot(T number);
}

// Generic calculator implementation
class GenericCalculator implements Calculator {
    private final Class type;
    private final BinaryOperator adder;
    private final BinaryOperator multiplier;
    
    public GenericCalculator(Class type, 
                            BinaryOperator adder,
                            BinaryOperator multiplier) {
        this.type = type;
        this.adder = adder;
        this.multiplier = multiplier;
    }
    
    @Override
    public T add(T a, T b) {
        return adder.apply(a, b);
    }
    
    @Override
    public T subtract(T a, T b) {
        // For simplicity, convert to double and back
        double result = a.doubleValue() - b.doubleValue();
        return convert(result);
    }
    
    @Override
    public T multiply(T a, T b) {
        return multiplier.apply(a, b);
    }
    
    @Override
    public T divide(T a, T b) {
        if (b.doubleValue() == 0) {
            throw new ArithmeticException("Division by zero");
        }
        double result = a.doubleValue() / b.doubleValue();
        return convert(result);
    }
    
    @Override
    public T power(T base, int exponent) {
        double result = Math.pow(base.doubleValue(), exponent);
        return convert(result);
    }
    
    @Override
    public T squareRoot(T number) {
        if (number.doubleValue() < 0) {
            throw new ArithmeticException("Square root of negative number");
        }
        double result = Math.sqrt(number.doubleValue());
        return convert(result);
    }
    
    private T convert(double value) {
        if (type == Integer.class) {
            return type.cast((int) Math.round(value));
        } else if (type == Double.class) {
            return type.cast(value);
        } else if (type == Float.class) {
            return type.cast((float) value);
        } else if (type == Long.class) {
            return type.cast((long) Math.round(value));
        }
        throw new UnsupportedOperationException("Unsupported type: " + type);
    }
}

// Statistics utility with generics
class Statistics> {
    private List data;
    
    public Statistics(List data) {
        this.data = new ArrayList<>(data);
    }
    
    public T getMin() {
        return data.stream().min(Comparable::compareTo).orElse(null);
    }
    
    public T getMax() {
        return data.stream().max(Comparable::compareTo).orElse(null);
    }
    
    public double getMean() {
        double sum = data.stream()
                        .mapToDouble(Number::doubleValue)
                        .sum();
        return sum / data.size();
    }
    
    public double getMedian() {
        List sorted = new ArrayList<>(data);
        Collections.sort(sorted);
        
        int size = sorted.size();
        if (size % 2 == 0) {
            double a = sorted.get(size/2 - 1).doubleValue();
            double b = sorted.get(size/2).doubleValue();
            return (a + b) / 2;
        } else {
            return sorted.get(size/2).doubleValue();
        }
    }
    
    public double getStandardDeviation() {
        double mean = getMean();
        double variance = data.stream()
                             .mapToDouble(n -> Math.pow(n.doubleValue() - mean, 2))
                             .sum() / data.size();
        return Math.sqrt(variance);
    }
}

public class GenericMathExample {
    public static void main(String[] args) {
        System.out.println("=== Generic Math Calculator Example ===\n");
        
        // Create calculators for different types
        Calculator intCalculator = new GenericCalculator<>(
            Integer.class,
            Integer::sum,
            (a, b) -> a * b
        );
        
        Calculator doubleCalculator = new GenericCalculator<>(
            Double.class,
            Double::sum,
            (a, b) -> a * b
        );
        
        // Test integer calculator
        System.out.println("Integer Calculator:");
        System.out.println("10 + 5 = " + intCalculator.add(10, 5));
        System.out.println("10 - 5 = " + intCalculator.subtract(10, 5));
        System.out.println("10 * 5 = " + intCalculator.multiply(10, 5));
        System.out.println("10 / 3 = " + intCalculator.divide(10, 3));
        System.out.println("2 ^ 8 = " + intCalculator.power(2, 8));
        System.out.println("√25 = " + intCalculator.squareRoot(25));
        
        // Test double calculator
        System.out.println("\nDouble Calculator:");
        System.out.println("10.5 + 5.5 = " + doubleCalculator.add(10.5, 5.5));
        System.out.println("10.5 - 5.5 = " + doubleCalculator.subtract(10.5, 5.5));
        System.out.println("10.5 * 2.0 = " + doubleCalculator.multiply(10.5, 2.0));
        System.out.println("10.0 / 3.0 = " + doubleCalculator.divide(10.0, 3.0));
        System.out.println("2.5 ^ 3 = " + doubleCalculator.power(2.5, 3));
        System.out.println("√30.25 = " + doubleCalculator.squareRoot(30.25));
        
        // Statistics example
        System.out.println("\n=== Statistics with Generics ===");
        
        List intData = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Statistics intStats = new Statistics<>(intData);
        
        System.out.println("Integer Statistics:");
        System.out.println("Min: " + intStats.getMin());
        System.out.println("Max: " + intStats.getMax());
        System.out.println("Mean: " + intStats.getMean());
        System.out.println("Median: " + intStats.getMedian());
        System.out.println("Std Dev: " + intStats.getStandardDeviation());
        
        List doubleData = Arrays.asList(1.5, 2.5, 3.5, 4.5, 5.5);
        Statistics doubleStats = new Statistics<>(doubleData);
        
        System.out.println("\nDouble Statistics:");
        System.out.println("Min: " + doubleStats.getMin());
        System.out.println("Max: " + doubleStats.getMax());
        System.out.println("Mean: " + doubleStats.getMean());
        System.out.println("Median: " + doubleStats.getMedian());
        
        // Generic utility method for numeric operations
        System.out.println("\n=== Generic Utility Methods ===");
        List numbers = Arrays.asList(1, 2.5, 3, 4.75, 5);
        
        System.out.println("Sum of numbers: " + sum(numbers));
        System.out.println("Average of numbers: " + average(numbers));
        
        // Generic method with bounded wildcard
        System.out.println("\nMaximum in collections:");
        System.out.println("Max int: " + findMax(intData));
        System.out.println("Max double: " + findMax(doubleData));
    }
    
    // Generic method with upper bounded wildcard
    public static double sum(List numbers) {
        return numbers.stream()
                     .mapToDouble(Number::doubleValue)
                     .sum();
    }
    
    public static double average(List numbers) {
        if (numbers.isEmpty()) return 0;
        return sum(numbers) / numbers.size();
    }
    
    // Generic method to find maximum
    public static > T findMax(List list) {
        if (list.isEmpty()) return null;
        
        T max = list.get(0);
        for (T item : list) {
            if (item.compareTo(max) > 0) {
                max = item;
            }
        }
        return max;
    }
}

8. Generics Best Practices

Common Generics Mistakes:
  1. Raw Types: Using generic types without type parameters
  2. Unchecked Casts: Using @SuppressWarnings without understanding
  3. Overusing Wildcards: Making APIs unnecessarily complex
  4. Ignoring Type Erasure: Assuming type information available at runtime
  5. Complex Type Hierarchies: Creating hard-to-understand generic code
  6. Not Using Bounds: Missing opportunities for type safety
Generics Best Practices
  • Always specify type parameters (avoid raw types)
  • Use meaningful type parameter names (T, E, K, V)
  • Apply PECS principle for maximum flexibility
  • Document type constraints in javadoc
  • Keep generic signatures simple and readable
  • Test with different type arguments
  • Consider performance implications of boxing/unboxing
Advanced Tips
  • Use Class<T> parameter when runtime type needed
  • Consider generic builders for complex object creation
  • Use type tokens for reified generics patterns
  • Leverage generic varargs carefully (T...)
  • Understand covariance/contravariance with wildcards
  • Use Optional for nullable generic returns
  • Consider using libraries like Guava for advanced generics

When to Use Generics

Use Generics when: Creating collections, utility classes, algorithms working with multiple types
Avoid Generics when: Simple classes with single type, performance-critical code with primitives
Consider Alternatives: For runtime type checking, consider Class objects or reflection
Rule of Thumb: If you find yourself casting, generics might help