Skip to content

Generics and Type Parameters in Java

Question

Explain the concept of generics in Java, including type parameters, bounded types, wildcards, and type erasure. How do you create and use generic classes and methods effectively?

Answer

Generics in Java provide type safety and enable code reuse by allowing classes, interfaces, and methods to operate on objects of various types while providing compile-time type checking.

Basic Generic Classes

  1. Simple Generic Class

    public class Box<T> {
        private T value;
    
        public Box(T value) {
            this.value = value;
        }
    
        public T getValue() {
            return value;
        }
    
        public void setValue(T value) {
            this.value = value;
        }
    }
    

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

Bounded Type Parameters

  1. Upper Bound

    public class NumberBox<T extends Number> {
        private T value;
    
        public NumberBox(T value) {
            this.value = value;
        }
    
        public double getDoubleValue() {
            return value.doubleValue();
        }
    }
    

  2. Multiple Bounds

    public class ComparableBox<T extends Number & Comparable<T>> {
        private T value;
    
        public ComparableBox(T value) {
            this.value = value;
        }
    
        public boolean isGreaterThan(ComparableBox<T> other) {
            return value.compareTo(other.value) > 0;
        }
    }
    

Generic Methods

  1. Basic Generic Method

    public class ArrayUtils {
        public static <T> T getFirstElement(T[] array) {
            if (array == null || array.length == 0) {
                throw new IllegalArgumentException("Array is empty or null");
            }
            return array[0];
        }
    }
    

  2. Bounded Generic Method

    public class CollectionUtils {
        public static <T extends Comparable<T>> T findMax(List<T> list) {
            if (list == null || list.isEmpty()) {
                throw new IllegalArgumentException("List is empty or null");
            }
            return list.stream()
                      .max(Comparable::compareTo)
                      .orElseThrow();
        }
    }
    

Wildcards

  1. Upper Bounded Wildcard

    public class NumberProcessor {
        public static double sum(List<? extends Number> numbers) {
            return numbers.stream()
                         .mapToDouble(Number::doubleValue)
                         .sum();
        }
    }
    

  2. Lower Bounded Wildcard

    public class CollectionCopier {
        public static <T> void copy(List<? super T> dest, List<? extends T> src) {
            dest.addAll(src);
        }
    }
    

Generic Interfaces

  1. Basic Generic Interface

    public interface Container<T> {
        void add(T item);
        T remove();
        boolean isEmpty();
    }
    
    public class Stack<T> implements Container<T> {
        private List<T> items = new ArrayList<>();
    
        @Override
        public void add(T item) {
            items.add(item);
        }
    
        @Override
        public T remove() {
            return items.remove(items.size() - 1);
        }
    
        @Override
        public boolean isEmpty() {
            return items.isEmpty();
        }
    }
    

  2. Generic Interface with Multiple Type Parameters

    public interface Map<K, V> {
        void put(K key, V value);
        V get(K key);
        boolean containsKey(K key);
    }
    

Type Erasure

  1. Understanding Type Erasure

    public class TypeErasureExample {
        // At runtime, T is erased to Object
        public static <T> void printType(T item) {
            System.out.println(item.getClass().getName());
        }
    
        // Type erasure with bounds
        public static <T extends Number> void printNumber(T number) {
            // T is erased to Number at runtime
            System.out.println(number.doubleValue());
        }
    }
    

  2. Bridge Methods

    public class GenericParent<T> {
        public void setValue(T value) {
            // Implementation
        }
    }
    
    public class StringChild extends GenericParent<String> {
        // Bridge method is automatically generated:
        // public void setValue(Object value) {
        //     setValue((String) value);
        // }
    }
    

Generic Collections

  1. Type-Safe Collections

    public class CollectionExample {
        public static void processStrings(List<String> strings) {
            for (String str : strings) {
                System.out.println(str.toUpperCase());
            }
        }
    
        public static <T> void processCollection(List<T> items) {
            for (T item : items) {
                System.out.println(item);
            }
        }
    }
    

  2. Generic Map Operations

    public class MapUtils {
        public static <K, V> Map<K, V> filterByValue(
            Map<K, V> map, Predicate<V> predicate) {
            return map.entrySet().stream()
                     .filter(entry -> predicate.test(entry.getValue()))
                     .collect(Collectors.toMap(
                         Map.Entry::getKey,
                         Map.Entry::getValue
                     ));
        }
    }
    

Best Practices

  1. Type Parameter Naming

    // Good - Clear type parameter names
    public class Container<E> {  // E for Element
        private E element;
    }
    
    public class Map<K, V> {    // K for Key, V for Value
        private K key;
        private V value;
    }
    
    // Bad - Unclear names
    public class Container<T> {  // T is too generic
        private T item;
    }
    

  2. Avoid Raw Types

    public class RawTypeExample {
        // Bad - Raw type
        List list = new ArrayList();
    
        // Good - Generic type
        List<String> list = new ArrayList<>();
    }
    

Common Pitfalls

  1. Type Erasure Limitations

    public class TypeErasureLimitations {
        // Bad - Cannot create array of generic type
        public static <T> T[] createArray() {
            return new T[10];  // Compilation error
        }
    
        // Good - Use List instead
        public static <T> List<T> createList() {
            return new ArrayList<>();
        }
    }
    

  2. Generic Method Overloading

    public class OverloadingExample {
        // Bad - Ambiguous due to type erasure
        public static void process(List<String> strings) {}
        public static void process(List<Integer> numbers) {}
    
        // Good - Different parameter types
        public static void processStrings(List<String> strings) {}
        public static void processNumbers(List<Integer> numbers) {}
    }
    

Modern Java Features

  1. Diamond Operator (Java 7+)

    public class DiamondExample {
        // Before Java 7
        List<String> list = new ArrayList<String>();
    
        // Java 7+
        List<String> list = new ArrayList<>();
    }
    

  2. Generic Type Inference

    public class TypeInference {
        public static <T> T createInstance(Class<T> clazz) {
            try {
                return clazz.getDeclaredConstructor().newInstance();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    
        // Usage with type inference
        String str = createInstance(String.class);
    }
    

Testing Generic Classes

  1. Testing Generic Methods

    @Test
    public void testGenericMethods() {
        Integer[] numbers = {1, 2, 3};
        assertEquals(Integer.valueOf(1), ArrayUtils.getFirstElement(numbers));
    
        String[] strings = {"a", "b", "c"};
        assertEquals("a", ArrayUtils.getFirstElement(strings));
    }
    

  2. Testing Generic Collections

    @Test
    public void testGenericCollections() {
        List<Integer> numbers = Arrays.asList(1, 2, 3);
        assertEquals(3, CollectionUtils.findMax(numbers));
    
        List<String> strings = Arrays.asList("a", "b", "c");
        assertEquals("c", CollectionUtils.findMax(strings));
    }