Generics in Depth in Java

Generics provide type safety and eliminate casting by allowing types (classes and interfaces) to be parameters when defining classes, interfaces, and methods.

1. Basic Generic Classes and Methods

Generic Classes

// Simple generic class public class Box<T> { private T content; public Box(T content) { this.content = content; } public T getContent() { return content; } public void setContent(T content) { this.content = content; } @Override public String toString() { return "Box containing: " + content; } } // 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; } public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } @Override public String toString() { return "Pair{" + key + "=" + value + "}"; } } // Generic class with bounds public class NumberBox<T extends Number> { private T number; public NumberBox(T number) { this.number = number; } public T getNumber() { return number; } public double doubleValue() { return number.doubleValue(); } }

Generic Methods

public class GenericMethods { // Simple generic method public static <T> T getFirst(T[] array) { if (array == null || array.length == 0) { return null; } return array[0]; } // Generic method with multiple type parameters public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) { return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue()); } // Bounded generic method public static <T extends Comparable<T>> T max(T a, T b) { return a.compareTo(b) > 0 ? a : b; } // Generic method with wildcard public static double sumOfList(List<? extends Number> list) { double sum = 0.0; for (Number num : list) { sum += num.doubleValue(); } return sum; } // Generic constructor (though rare) public <T> GenericMethods(T item) { System.out.println("Created with: " + item); } }

Usage Examples

public class BasicGenericsDemo { public static void main(String[] args) { // Generic class usage Box<String> stringBox = new Box<>("Hello Generics"); Box<Integer> integerBox = new Box<>(42); System.out.println(stringBox.getContent().toUpperCase()); // No casting needed System.out.println(integerBox.getContent() + 10); // No unboxing needed // Pair usage Pair<String, Integer> nameAge = new Pair<>("John", 25); Pair<String, String> keyValue = new Pair<>("config", "value"); // Generic methods usage String[] names = {"Alice", "Bob", "Charlie"}; String first = GenericMethods.getFirst(names); Integer max = GenericMethods.max(10, 20); // NumberBox with bounds NumberBox<Integer> intBox = new NumberBox<>(100); NumberBox<Double> doubleBox = new NumberBox<>(3.14); // NumberBox<String> stringBox = new NumberBox<>("test"); // Compile error! } }

2. Type Bounds and Constraints

Upper Bounds

import java.util.*; public class BoundedGenerics { // Single upper bound public static <T extends Number> double sum(List<T> numbers) { double total = 0.0; for (T num : numbers) { total += num.doubleValue(); } return total; } // Multiple bounds public static <T extends Comparable<T> & Cloneable> T minAndClone(T a, T b) { T result = a.compareTo(b) < 0 ? a : b; // Can call clone() because T extends Cloneable try { return (T) result.getClass().getMethod("clone").invoke(result); } catch (Exception e) { throw new RuntimeException("Clone failed", e); } } // Class with multiple type bounds public static class ComparablePair<T extends Comparable<T>, U extends Comparable<U>> implements Comparable<ComparablePair<T, U>> { private T first; private U second; public ComparablePair(T first, U second) { this.first = first; this.second = second; } @Override public int compareTo(ComparablePair<T, U> other) { int firstCompare = this.first.compareTo(other.first); if (firstCompare != 0) { return firstCompare; } return this.second.compareTo(other.second); } } } // Interface bounds example interface Auditable { String getAuditLog(); } class AuditableEntity<T extends Auditable> { private T entity; public AuditableEntity(T entity) { this.entity = entity; } public void audit() { System.out.println("Audit: " + entity.getAuditLog()); } }

Lower Bounds (in Wildcards)

import java.util.*; public class WildcardBounds { // Upper bounded wildcard - READ mostly public static void processNumbers(List<? extends Number> numbers) { for (Number num : numbers) { System.out.println(num.doubleValue()); } // numbers.add(new Integer(1)); // COMPILE ERROR - cannot add } // Lower bounded wildcard - WRITE mostly public static void addIntegers(List<? super Integer> list) { for (int i = 1; i <= 5; i++) { list.add(i); // Can add Integers } // Integer value = list.get(0); // COMPILE ERROR - can only get Object } // Unbounded wildcard - when you don't care about type public static void printList(List<?> list) { for (Object elem : list) { System.out.println(elem); } // list.add(new Object()); // COMPILE ERROR - cannot add } // Complex example with multiple bounds public static <T> void copy(List<? super T> dest, List<? extends T> src) { for (T item : src) { dest.add(item); } } }

3. Advanced Generic Features

Generic Inheritance and Subtyping

// Generic class hierarchy class BaseEntity<T> { protected T id; public BaseEntity(T id) { this.id = id; } public T getId() { return id; } } class User extends BaseEntity<Long> { private String name; public User(Long id, String name) { super(id); this.name = name; } // Can use Long directly because T is bound to Long public boolean hasValidId() { return id != null && id > 0; } } // Generic interface interface Repository<T, ID> { T findById(ID id); void save(T entity); void delete(ID id); } // Implementing generic interface class UserRepository implements Repository<User, Long> { private Map<Long, User> storage = new HashMap<>(); @Override public User findById(Long id) { return storage.get(id); } @Override public void save(User user) { storage.put(user.getId(), user); } @Override public void delete(Long id) { storage.remove(id); } } // Multiple interface implementation class AdvancedRepository<T, ID> implements Repository<T, ID>, Iterable<T> { private List<T> items = new ArrayList<>(); @Override public T findById(ID id) { // Implementation return null; } @Override public void save(T entity) { items.add(entity); } @Override public void delete(ID id) { // Implementation } @Override public Iterator<T> iterator() { return items.iterator(); } }

Recursive Generic Types

// Self-referential generic type interface Comparable<T> { int compareTo(T other); } // Recursive bound - common in builder pattern abstract class Animal<T extends Animal<T>> implements Comparable<T> { protected String name; protected int weight; public T setName(String name) { this.name = name; return self(); } public T setWeight(int weight) { this.weight = weight; return self(); } @Override public int compareTo(T other) { return Integer.compare(this.weight, other.weight); } // Abstract method to return 'this' with correct type protected abstract T self(); } class Dog extends Animal<Dog> { private String breed; public Dog setBreed(String breed) { this.breed = breed; return this; } @Override protected Dog self() { return this; } // Method chaining works with correct types public static void example() { Dog dog = new Dog() .setName("Buddy") .setWeight(25) .setBreed("Golden Retriever"); } }

4. Type Erasure and Bridge Methods

Understanding Type Erasure

import java.lang.reflect.*; import java.util.*; public class TypeErasureDemo { // After compilation, generics are erased public static class ErasedBox { private Object content; // T becomes Object public ErasedBox(Object content) { this.content = content; } public Object getContent() { return content; } } // Demonstration of type erasure public static void demonstrateErasure() { List<String> stringList = new ArrayList<>(); List<Integer> integerList = new ArrayList<>(); // Both have same class due to type erasure System.out.println("String list class: " + stringList.getClass()); System.out.println("Integer list class: " + integerList.getClass()); System.out.println("Same class? " + (stringList.getClass() == integerList.getClass())); // Cannot use instanceof with generics // if (stringList instanceof List<String>) {} // Compile error // Raw type usage List rawList = stringList; // Warning but allowed rawList.add(42); // Runtime error - ClassCastException later } // Bridge methods example public static class ComparableBox<T extends Comparable<T>> implements Comparable<ComparableBox<T>> { private T value; public ComparableBox(T value) { this.value = value; } // The compiler generates a bridge method: // public int compareTo(Object other) { // return compareTo((ComparableBox) other); // } @Override public int compareTo(ComparableBox<T> other) { return this.value.compareTo(other.value); } } // Reflection to examine generic types public static void examineGenerics() throws Exception { List<String> stringList = Arrays.asList("a", "b", "c"); // Get actual type parameters through reflection Type type = stringList.getClass().getGenericSuperclass(); if (type instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) type; Type[] actualTypes = pt.getActualTypeArguments(); System.out.println("Actual type arguments: " + Arrays.toString(actualTypes)); } // Method parameter types Method method = TypeErasureDemo.class.getMethod("genericMethod", List.class); Type[] paramTypes = method.getGenericParameterTypes(); for (Type paramType : paramTypes) { System.out.println("Parameter type: " + paramType); } } public static <T> void genericMethod(List<T> list) { // Method implementation } }

5. Generic Constraints and Limitations

Common Limitations

import java.util.*; public class GenericLimitations { // 1. Cannot instantiate type parameters public static <T> void createInstance() { // T obj = new T(); // COMPILE ERROR // T[] array = new T[10]; // COMPILE ERROR } // Workaround using Class<T> public static <T> T createInstance(Class<T> clazz) throws Exception { return clazz.newInstance(); } // 2. Cannot use primitives as type arguments // List<int> primitiveList; // COMPILE ERROR List<Integer> boxedList; // Use wrapper classes // 3. Cannot create arrays of parameterized types public static void arrayLimitations() { // List<String>[] arrayOfLists = new List<String>[10]; // COMPILE ERROR List<String>[] arrayOfLists = (List<String>[]) new List<?>[10]; // Warning // But you can use wildcard arrays List<?>[] wildcardArray = new List<?>[10]; } // 4. Cannot use instanceof with parameterized types public static boolean checkInstance(Object obj) { // if (obj instanceof List<String>) { } // COMPILE ERROR if (obj instanceof List) { List<?> list = (List<?>) obj; // Check elements individually for (Object item : list) { if (!(item instanceof String)) { return false; } } return true; } return false; } // 5. Cannot overload methods with same erasure public class OverloadProblem { // public void process(List<String> list) { } // COMPILE ERROR // public void process(List<Integer> list) { } // Same erasure: process(List) // Workaround - use different method names or parameters public void processStrings(List<String> list) { } public void processIntegers(List<Integer> list) { } } // 6. Cannot catch or throw parameterized exceptions public static class ExceptionLimitations { // public static <T extends Exception> void problematic() { // try { // // some code // } catch (T e) { // COMPILE ERROR // // handle exception // } // } } }

6. Advanced Patterns and Real-World Examples

Generic DAO Pattern

// Generic Data Access Object pattern public interface GenericDao<T, ID> { T findById(ID id); List<T> findAll(); T save(T entity); void delete(ID id); List<T> findByCriteria(Criteria criteria); } // Abstract implementation public abstract class AbstractJpaDao<T, ID> implements GenericDao<T, ID> { private final Class<T> persistentClass; protected AbstractJpaDao(Class<T> persistentClass) { this.persistentClass = persistentClass; } protected abstract EntityManager getEntityManager(); @Override public T findById(ID id) { return getEntityManager().find(persistentClass, id); } @Override @SuppressWarnings("unchecked") public List<T> findAll() { String query = "SELECT e FROM " + persistentClass.getSimpleName() + " e"; return getEntityManager().createQuery(query).getResultList(); } @Override public T save(T entity) { getEntityManager().persist(entity); return entity; } @Override public void delete(ID id) { T entity = findById(id); if (entity != null) { getEntityManager().remove(entity); } } } // Concrete implementation public class UserDao extends AbstractJpaDao<User, Long> { public UserDao() { super(User.class); } @Override protected EntityManager getEntityManager() { // Return EntityManager instance return null; } // Type-safe custom queries public List<User> findByName(String name) { return getEntityManager() .createQuery("SELECT u FROM User u WHERE u.name = :name", User.class) .setParameter("name", name) .getResultList(); } }

Builder Pattern with Generics

// Generic builder pattern public abstract class GenericBuilder<T> { protected T instance; protected abstract T createInstance(); public GenericBuilder() { this.instance = createInstance(); } public T build() { try { return instance; } finally { this.instance = createInstance(); // Reset for reuse } } } // Concrete builder public class PersonBuilder extends GenericBuilder<Person> { @Override protected Person createInstance() { return new Person(); } public PersonBuilder withName(String name) { instance.setName(name); return this; } public PersonBuilder withAge(int age) { instance.setAge(age); return this; } public PersonBuilder withEmail(String email) { instance.setEmail(email); return this; } } // Usage public class BuilderDemo { public static void main(String[] args) { Person person = new PersonBuilder() .withName("John Doe") .withAge(30) .withEmail("[email protected]") .build(); } }

Event System with Generics

// Generic event system public interface EventListener<T> { void onEvent(T event); } public class EventBus { private final Map<Class<?>, List<EventListener<?>>> listeners = new HashMap<>(); public <T> void register(Class<T> eventType, EventListener<T> listener) { listeners.computeIfAbsent(eventType, k -> new ArrayList<>()).add(listener); } @SuppressWarnings("unchecked") public <T> void publish(T event) { Class<?> eventType = event.getClass(); List<EventListener<?>> eventListeners = listeners.get(eventType); if (eventListeners != null) { for (EventListener listener : eventListeners) { listener.onEvent(event); } } } } // Event classes class UserRegisteredEvent { private final String username; private final long timestamp; public UserRegisteredEvent(String username) { this.username = username; this.timestamp = System.currentTimeMillis(); } // getters... } class OrderCreatedEvent { private final String orderId; private final double amount; public OrderCreatedEvent(String orderId, double amount) { this.orderId = orderId; this.amount = amount; } // getters... } // Usage public class EventSystemDemo { public static void main(String[] args) { EventBus bus = new EventBus(); bus.register(UserRegisteredEvent.class, (UserRegisteredEvent event) -> { System.out.println("User registered: " + event.getUsername()); }); bus.register(OrderCreatedEvent.class, (OrderCreatedEvent event) -> { System.out.println("Order created: " + event.getOrderId()); }); // Type-safe event publishing bus.publish(new UserRegisteredEvent("john_doe")); bus.publish(new OrderCreatedEvent("ORD-123", 99.99)); } }

7. Best Practices and Common Pitfalls

import java.util.*; public class GenericsBestPractices { // 1. Use descriptive type parameter names public interface Repository<E, ID> { // E for Entity, ID for Identifier E findById(ID id); } // 2. Prefer generic methods over raw types public static class GoodPractice { // Good - generic method public static <T> T firstElement(List<T> list) { return list.isEmpty() ? null : list.get(0); } // Bad - raw types public static Object firstElementRaw(List list) { return list.isEmpty() ? null : list.get(0); } } // 3. Use bounded wildcards for maximum flexibility public static class FlexibleAPI { // Producer - uses extends public static void processItems(List<? extends Number> items) { for (Number item : items) { System.out.println(item.doubleValue()); } } // Consumer - uses super public static void addNumbers(List<? super Integer> list) { list.add(1); list.add(2); list.add(3); } } // 4. Avoid unchecked warnings @SuppressWarnings("unchecked") public static <T> T[] createArray(Class<T> clazz, int size) { // This is one of the few places where @SuppressWarnings is acceptable return (T[]) java.lang.reflect.Array.newInstance(clazz, size); } // 5. Use generic types in method signatures public static <K, V> Map<K, V> createMap() { return new HashMap<>(); } // 6. Be careful with varargs and generics @SafeVarargs public static <T> List<T> asList(T... elements) { List<T> list = new ArrayList<>(); for (T element : elements) { list.add(element); } return list; } } // Common anti-patterns to avoid public class GenericsAntiPatterns { // 1. Raw types - DON'T DO THIS List rawList = new ArrayList(); // Warning // 2. Unnecessary bounds class OverlyConstrained<T extends Object> { } // Redundant // 3. Mixing generics with arrays public static <T> void problematic(T[] array) { // Object[] objArray = array; // objArray[0] = new Object(); // ArrayStoreException at runtime } // 4. Ignoring compiler warnings @SuppressWarnings({"rawtypes", "unchecked"}) public static void ignoringWarnings() { List list = new ArrayList(); list.add("string"); list.add(1); // No compile error, but runtime issues } }

Key Takeaways

  1. Type Safety: Generics provide compile-time type checking
  2. Code Reuse: Write generic algorithms that work with different types
  3. No Casting: Eliminate explicit casting in your code
  4. Type Erasure: Generics are erased at runtime, remember the limitations
  5. Wildcards: Use ? extends for producers and ? super for consumers
  6. Bounds: Constrain type parameters to specific hierarchies
  7. Best Practices: Follow naming conventions and avoid raw types

Generics are a powerful feature that, when used correctly, can make your code more type-safe, readable, and maintainable.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper