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.