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
- Type Safety: Generics provide compile-time type checking
- Code Reuse: Write generic algorithms that work with different types
- No Casting: Eliminate explicit casting in your code
- Type Erasure: Generics are erased at runtime, remember the limitations
- Wildcards: Use
? extendsfor producers and? superfor consumers - Bounds: Constrain type parameters to specific hierarchies
- 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.