ð java-generics
Use when Java generics including type parameters, wildcards, and type bounds. Use when writing type-safe reusable code.
Overview
Master Java's generics system for writing type-safe, reusable code with compile-time type checking, generic classes, methods, wildcards, and type bounds.
Introduction to Generics
Generics enable types to be parameters when defining classes, interfaces, and methods, providing compile-time type safety.
Basic generic class:
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
public static void main(String[] args) {
// Type-safe box for String
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String value = stringBox.get(); // No casting needed
// Type-safe box for Integer
Box<Integer> intBox = new Box<>();
intBox.set(42);
Integer number = intBox.get();
}
}
Generic with 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 static void main(String[] args) {
Pair<String, Integer> pair = new Pair<>("age", 30);
String key = pair.getKey();
Integer value = pair.getValue();
}
}
Generic Methods
Generic methods can be defined independently of generic classes.
Basic generic method:
public class GenericMethods {
// Generic method
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
// Generic method with return type
public static <T> T getFirst(T[] array) {
if (array.length > 0) {
return array[0];
}
return null;
}
public static void main(String[] args) {
String[] strings = {"a", "b", "c"};
Integer[] numbers = {1, 2, 3};
printArray(strings); // T inferred as String
printArray(numbers); // T inferred as Integer
String first = getFirst(strings);
Integer firstNum = getFirst(numbers);
}
}
Generic method with multiple type parameters:
public class MultiplTypeParams {
public static <K, V> Map<K, V> createMap(K key, V value) {
Map<K, V> map = new HashMap<>();
map.put(key, value);
return map;
}
public static <T, R> R transform(T input, Function<T, R> transformer) {
return transformer.apply(input);
}
public static void main(String[] args) {
Map<String, Integer> map = createMap("count", 10);
String result = transform(42, num -> "Number: " + num);
// Result: "Number: 42"
}
}
Bounded Type Parameters
Type bounds restrict the types that can be used as type arguments.
Upper bounded type parameters:
public class UpperBound {
// T must be Number or subclass of Number
public static <T extends Number> double sum(List<T> numbers) {
double total = 0;
for (T num : numbers) {
total += num.doubleValue();
}
return total;
}
// Multiple bounds
public static <T extends Comparable<T> & Serializable> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
public static void main(String[] args) {
List<Integer> integers = List.of(1, 2, 3, 4, 5);
double sum = sum(integers); // 15.0
List<Double> doubles = List.of(1.5, 2.5, 3.5);
double doubleSum = sum(doubles); // 7.5
String maxStr = max("apple", "banana"); // "banana"
}
}
Class with bounded type parameter:
public class NumberBox<T extends Number> {
private T number;
public NumberBox(T number) {
this.number = number;
}
public double doubleValue() {
return number.doubleValue();
}
public boolean isZero() {
return number.doubleValue() == 0.0;
}
public static void main(String[] args) {
NumberBox<Integer> intBox = new NumberBox<>(42);
NumberBox<Double> doubleBox = new NumberBox<>(3.14);
// Compile error: String is not a Number
// NumberBox<String> stringBox = new NumberBox<>("fail");
}
}
Wildcards
Wildcards provide flexibility when working with generic types.
Unbounded wildcard:
public class UnboundedWildcard {
// Accept any List
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.println(elem);
}
}
public static int size(List<?> list) {
return list.size();
}
public static void main(String[] args) {
List<String> strings = List.of("a", "b", "c");
List<Integer> integers = List.of(1, 2, 3);
printList(strings);
printList(integers);
System.out.println(size(strings)); // 3
System.out.println(size(integers)); // 3
}
}
Upper bounded wildcard:
public class UpperBoundedWildcard {
// Accept List of Number or any subclass
public static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number num : numbers) {
total += num.doubleValue();
}
return total;
}
public static void main(String[] args) {
List<Integer> integers = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5);
List<Number> numbers = List.of(1, 2.5, 3);
System.out.println(sum(integers)); // 6.0
System.out.println(sum(doubles)); // 4.0
System.out.println(sum(numbers)); // 6.5
}
}
Lower bounded wildcard:
public class LowerBoundedWildcard {
// Accept List of Integer or any superclass
public static void addIntegers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++) {
list.add(i);
}
}
public static void main(String[] args) {
List<Integer> integers = new ArrayList<>();
addIntegers(integers);
System.out.println(integers); // [1, 2, 3, 4, 5]
List<Number> numbers = new ArrayList<>();
addIntegers(numbers);
System.out.println(numbers); // [1, 2, 3, 4, 5]
List<Object> objects = new ArrayList<>();
addIntegers(objects);
System.out.println(objects); // [1, 2, 3, 4, 5]
}
}
PECS Principle
Producer Extends, Consumer Super - guideline for using wildcards.
PECS in action:
public class PECSExample {
// Producer - reading from source (extends)
public static <T> void copy(
List<? extends T> source,
List<? super T> destination
) {
for (T item : source) {
destination.add(item);
}
}
// Producer - extends for reading
public static double sumNumbers(List<? extends Number> numbers) {
double sum = 0;
for (Number num : numbers) { // Reading (producing values)
sum += num.doubleValue();
}
return sum;
}
// Consumer - super for writing
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 3; i++) {
list.add(i); // Writing (consuming values)
}
}
public static void main(String[] args) {
List<Integer> source = List.of(1, 2, 3);
List<Number> destination = new ArrayList<>();
copy(source, destination);
System.out.println(destination); // [1, 2, 3]
}
}
Generic Interfaces
Interfaces can be generic, providing contracts for generic types.
Generic interface:
public interface Repository<T, ID> {
T findById(ID id);
List<T> findAll();
void save(T entity);
void delete(ID id);
}
public 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 List<User> findAll() {
return new ArrayList<>(storage.values());
}
@Override
public void save(User user) {
storage.put(user.getId(), user);
}
@Override
public void delete(Long id) {
storage.remove(id);
}
}
class User {
private Long id;
private String name;
public User(Long id, String name) {
this.id = id;
this.name = name;
}
public Long getId() { return id; }
public String getName() { return name; }
}
Comparable and Comparator:
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name);
}
public static void main(String[] args) {
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 30));
people.add(new Person("Bob", 25));
// Natural ordering (by name)
Collections.sort(people);
// Custom comparator (by age)
Comparator<Person> ageComparator =
Comparator.comparingInt(p -> p.age);
people.sort(ageComparator);
}
}
Type Erasure
Java generics use type erasure - generic type information is removed at runtime.
Understanding type erasure:
public class TypeErasure {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
// At runtime, both are just List
System.out.println(strings.getClass() == integers.getClass());
// true
// Cannot check generic type at runtime
// if (list instanceof List<String>) {} // Compile error
// Can only check raw type
if (strings instanceof List) {
System.out.println("Is a List");
}
}
}
Consequences of type erasure:
public class ErasureConsequences<T> {
// Cannot create instance of type parameter
// T instance = new T(); // Compile error
// Cannot create array of parameterized type
// T[] array = new T[10]; // Compile error
// Cannot use instanceof with type parameter
public boolean isInstance(Object obj) {
// if (obj instanceof T) {} // Compile error
return true;
}
// Workaround: pass Class<T>
private Class<T> type;
public ErasureConsequences(Class<T> type) {
this.type = type;
}
public T createInstance() throws Exception {
return type.getDeclaredConstructor().newInstance();
}
@SuppressWarnings("unchecked")
public T[] createArray(int size) {
return (T[]) Array.newInstance(type, size);
}
}
Generic Builders
Builder pattern with generics for fluent APIs.
Generic builder:
public class Query<T> {
private final Class<T> type;
private String where;
private String orderBy;
private int limit;
private Query(Class<T> type) {
this.type = type;
}
public static <T> Query<T> from(Class<T> type) {
return new Query<>(type);
}
public Query<T> where(String condition) {
this.where = condition;
return this;
}
public Query<T> orderBy(String field) {
this.orderBy = field;
return this;
}
public Query<T> limit(int count) {
this.limit = count;
return this;
}
public List<T> execute() {
// Execute query and return results
return new ArrayList<>();
}
public static void main(String[] args) {
List<User> users = Query.from(User.class)
.where("age > 18")
.orderBy("name")
.limit(10)
.execute();
}
}
Recursive Type Bounds
Type bounds can reference the type parameter itself.
Enum with recursive bound:
public class RecursiveBound {
// Enum trick
public static <E extends Enum<E>> void printEnum(Class<E> enumClass) {
for (E constant : enumClass.getEnumConstants()) {
System.out.println(constant);
}
}
// Comparable with recursive bound
public static <T extends Comparable<T>> T max(List<T> list) {
if (list.isEmpty()) {
throw new IllegalArgumentException("Empty list");
}
T max = list.get(0);
for (T item : list) {
if (item.compareTo(max) > 0) {
max = item;
}
}
return max;
}
enum Color { RED, GREEN, BLUE }
public static void main(String[] args) {
printEnum(Color.class);
List<String> words = List.of("apple", "banana", "cherry");
String maxWord = max(words); // "cherry"
List<Integer> numbers = List.of(1, 5, 3, 9, 2);
Integer maxNum = max(numbers); // 9
}
}
Builder with recursive bound:
public abstract class Builder<T, B extends Builder<T, B>> {
protected abstract B self();
public abstract T build();
}
public class Person {
private final String name;
private final int age;
protected Person(PersonBuilder<?> builder) {
this.name = builder.name;
this.age = builder.age;
}
public static PersonBuilder<?> builder() {
return new PersonBuilder<>();
}
public static class PersonBuilder<B extends PersonBuilder<B>>
extends Builder<Person, B> {
private String name;
private int age;
public B name(String name) {
this.name = name;
return self();
}
public B age(int age) {
this.age = age;
return self();
}
@Override
@SuppressWarnings("unchecked")
protected B self() {
return (B) this;
}
@Override
public Person build() {
return new Person(this);
}
}
}
When to Use This Skill
Use java-generics when you need to:
- Write reusable code that works with multiple types
- Enforce compile-time type safety
- Eliminate casting and type errors at runtime
- Create generic collections, algorithms, or utilities
- Build type-safe APIs and frameworks
- Implement generic design patterns
- Work with Java Collections Framework
- Define flexible method signatures with type parameters
- Create bounded type hierarchies
- Implement builder or factory patterns with type safety
Best Practices
- Use meaningful type parameter names (T, E, K, V)
- Prefer bounded type parameters over raw types
- Use wildcards for flexibility in method parameters
- Apply PECS principle (Producer Extends, Consumer Super)
- Avoid raw types in new code
- Use @SuppressWarnings("unchecked") sparingly
- Document generic type constraints clearly
- Prefer generic methods over generic classes when possible
- Use bounded wildcards for maximum API flexibility
- Consider type erasure implications
Common Pitfalls
- Using raw types instead of parameterized types
- Confusing extends and super wildcards
- Trying to create arrays of generic types
- Not understanding type erasure limitations
- Overusing wildcards making code unreadable
- Incorrect variance with wildcards
- Forgetting that generics are compile-time only
- Not handling unchecked warnings properly
- Creating unnecessarily complex generic hierarchies
- Misusing instanceof with generic types