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
-
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; } }
-
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
-
Upper Bound
public class NumberBox<T extends Number> { private T value; public NumberBox(T value) { this.value = value; } public double getDoubleValue() { return value.doubleValue(); } }
-
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
-
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]; } }
-
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
-
Upper Bounded Wildcard
public class NumberProcessor { public static double sum(List<? extends Number> numbers) { return numbers.stream() .mapToDouble(Number::doubleValue) .sum(); } }
-
Lower Bounded Wildcard
public class CollectionCopier { public static <T> void copy(List<? super T> dest, List<? extends T> src) { dest.addAll(src); } }
Generic Interfaces
-
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(); } }
-
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
-
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()); } }
-
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
-
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); } } }
-
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
-
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; }
-
Avoid Raw Types
public class RawTypeExample { // Bad - Raw type List list = new ArrayList(); // Good - Generic type List<String> list = new ArrayList<>(); }
Common Pitfalls
-
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<>(); } }
-
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
-
Diamond Operator (Java 7+)
public class DiamondExample { // Before Java 7 List<String> list = new ArrayList<String>(); // Java 7+ List<String> list = new ArrayList<>(); }
-
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
-
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)); }
-
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)); }