Generics in Java
Question
Explain the concept of generics in Java. How do you create generic classes, methods, and interfaces? What are type bounds, wildcards, and type erasure? What are the best practices for using generics?
Answer
Generics in Java provide type safety and eliminate the need for explicit type casting. They allow you to write code that can work with different data types while maintaining type safety at compile time.
Generic Classes
-
Basic 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; } } // Usage Box<String> stringBox = new Box<>("Hello"); Box<Integer> intBox = new Box<>(42);
-
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; } } // Usage Pair<String, Integer> pair = new Pair<>("Age", 25);
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]; } public static <T> void swap(T[] array, int i, int j) { T temp = array[i]; array[i] = array[j]; array[j] = temp; } } // Usage String[] strings = {"Hello", "World"}; Integer[] numbers = {1, 2, 3}; String firstString = ArrayUtils.getFirstElement(strings); Integer firstNumber = ArrayUtils.getFirstElement(numbers);
-
Generic Method with Type Bounds
public class NumberUtils { public static <T extends Number> double sum(T[] numbers) { double sum = 0.0; for (T number : numbers) { sum += number.doubleValue(); } return sum; } } // Usage Integer[] integers = {1, 2, 3, 4, 5}; Double[] doubles = {1.1, 2.2, 3.3}; double intSum = NumberUtils.sum(integers); double doubleSum = NumberUtils.sum(doubles);
Generic Interfaces
- Basic Generic Interface
public interface Container<T> { void add(T item); T remove(); boolean isEmpty(); int size(); } 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(); } @Override public int size() { return items.size(); } }
Type Bounds
-
Upper Bounds
public class Animal { public void makeSound() { System.out.println("Some sound"); } } public class Dog extends Animal { @Override public void makeSound() { System.out.println("Woof!"); } } public class AnimalContainer<T extends Animal> { private T animal; public AnimalContainer(T animal) { this.animal = animal; } public void makeSound() { animal.makeSound(); } }
-
Multiple Bounds
public interface Comparable<T> { int compareTo(T other); } public interface Serializable { void serialize(); } public class DataProcessor<T extends Comparable<T> & Serializable> { private T data; public void process() { data.serialize(); } }
Wildcards
-
Upper Bounded Wildcard
public class NumberProcessor { public static double sum(List<? extends Number> numbers) { return numbers.stream() .mapToDouble(Number::doubleValue) .sum(); } } // Usage List<Integer> integers = Arrays.asList(1, 2, 3); List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3); double sum1 = NumberProcessor.sum(integers); double sum2 = NumberProcessor.sum(doubles);
-
Lower Bounded Wildcard
public class CollectionUtils { public static void addNumbers(List<? super Integer> list) { list.add(1); list.add(2); list.add(3); } } // Usage List<Number> numbers = new ArrayList<>(); List<Object> objects = new ArrayList<>(); CollectionUtils.addNumbers(numbers); CollectionUtils.addNumbers(objects);
Type Erasure
- Understanding Type Erasure
public class GenericExample<T> { private T value; public void setValue(T value) { this.value = value; } public T getValue() { return value; } } // After type erasure public class GenericExample { private Object value; public void setValue(Object value) { this.value = value; } public Object getValue() { return value; } }
Best Practices
-
Type Safety
public class SafeContainer<T> { private final List<T> items = new ArrayList<>(); public void add(T item) { if (item == null) { throw new IllegalArgumentException("Item cannot be null"); } items.add(item); } public T get(int index) { if (index < 0 || index >= items.size()) { throw new IndexOutOfBoundsException(); } return items.get(index); } }
-
Generic Method Overloading
public class OverloadExample { public static <T> void process(T item) { System.out.println("Processing: " + item); } public static <T extends Number> void process(T number) { System.out.println("Processing number: " + number); } }
Common Use Cases
-
Generic Collections
public class Cache<K, V> { private final Map<K, V> cache = new HashMap<>(); public void put(K key, V value) { cache.put(key, value); } public V get(K key) { return cache.get(key); } public void remove(K key) { cache.remove(key); } }
-
Generic Builder Pattern
public class Builder<T> { private final List<T> items = new ArrayList<>(); public Builder<T> add(T item) { items.add(item); return this; } public List<T> build() { return new ArrayList<>(items); } } // Usage List<String> strings = new Builder<String>() .add("Hello") .add("World") .build();
Testing
- Testing Generic Classes
@Test public void testBox() { Box<String> stringBox = new Box<>("Test"); assertEquals("Test", stringBox.getValue()); Box<Integer> intBox = new Box<>(42); assertEquals(42, intBox.getValue()); } @Test public void testPair() { Pair<String, Integer> pair = new Pair<>("Age", 25); assertEquals("Age", pair.getKey()); assertEquals(25, pair.getValue()); }
Common Pitfalls
-
Raw Types
// Bad - Using raw types public void badPractice() { Box box = new Box("Hello"); // Raw type String value = (String) box.getValue(); // Unsafe cast } // Good - Using generics public void goodPractice() { Box<String> box = new Box<>("Hello"); String value = box.getValue(); // Type-safe }
-
Type Erasure Limitations
// Bad - Trying to create generic array public class BadExample<T> { private T[] array = new T[10]; // Compilation error } // Good - Using List instead public class GoodExample<T> { private List<T> list = new ArrayList<>(); }