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