Skip to content

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

  1. 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);
    

  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; }
    }
    
    // Usage
    Pair<String, Integer> pair = new Pair<>("Age", 25);
    

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];
        }
    
        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);
    

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

  1. 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

  1. 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();
        }
    }
    

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

  1. 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);
    

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

  1. 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

  1. 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);
        }
    }
    

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

  1. 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);
        }
    }
    

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

  1. 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

  1. 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
    }
    

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