Java Generics¶
Overview¶
Java Generics, introduced in Java 5 (2004), provides a way to create classes, interfaces, and methods that operate on a type parameter. The primary motivation behind generics is to enable programmers to create type-safe collections while providing compile-time type checking and eliminating the need for explicit casting. Generics allow you to abstract over types, creating reusable components that work with different data types while maintaining type safety.
Prerequisites¶
- Basic Java programming knowledge
- Understanding of object-oriented programming concepts
- Familiarity with Java collections framework
- Understanding of Java class hierarchy and polymorphism
Learning Objectives¶
- Understand the purpose and benefits of generics in Java
- Learn to create and use generic classes and interfaces
- Master type parameters, bounds, and wildcards
- Understand the limitations imposed by type erasure
- Apply generics effectively with Java collections
- Implement generic methods to create flexible, reusable code
- Recognize and navigate common generics pitfalls
- Apply best practices for working with generics
Table of Contents¶
- Introduction to Generics
- Generic Classes and Interfaces
- Type Parameters and Naming Conventions
- Type Parameter Bounds
- Generic Methods
- Wildcards
- Type Erasure
- Generics and Collections
- Generics and Inheritance
- Raw Types and Backward Compatibility
- Advanced Generic Patterns
Introduction to Generics¶
Benefits of Generics¶
Generics provide several important benefits for Java programs:
- Type Safety: Generics enable compile-time type checking, preventing ClassCastExceptions at runtime.
- Elimination of Casts: No need for explicit casting when retrieving elements from collections.
- Code Reusability: Write code once that works with different types.
- Higher Abstraction: Express algorithms independently of specific types.
Before and After Generics¶
Let's compare code with and without generics:
// Before generics (pre-Java 5)
List list = new ArrayList();
list.add("Hello");
list.add(42); // No type checking, can add anything
String s = (String) list.get(0); // Explicit casting required
Integer i = (Integer) list.get(1);
String error = (String) list.get(1); // Runtime ClassCastException
// With generics (Java 5+)
List<String> list = new ArrayList<>();
list.add("Hello");
// list.add(42); // Compile-time error
String s = list.get(0); // No casting needed
Generic Classes and Interfaces¶
Creating a Generic Class¶
A generic class is declared with one or more type parameters enclosed in angle brackets.
// Generic class with one type parameter
public class Box<T> {
private T item;
public void put(T item) {
this.item = item;
}
public T get() {
return item;
}
}
// Usage
Box<String> stringBox = new Box<>();
stringBox.put("Hello Generics");
String str = stringBox.get(); // No casting needed
Box<Integer> intBox = new Box<>();
intBox.put(42);
Integer num = intBox.get();
Generic Class with Multiple Type Parameters¶
// Generic class with two 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;
}
@Override
public String toString() {
return "(" + key + ", " + value + ")";
}
}
// Usage
Pair<String, Integer> pair = new Pair<>("Age", 30);
String key = pair.getKey(); // "Age"
Integer value = pair.getValue(); // 30
Generic Interfaces¶
Interfaces can also be generic, allowing for type-safe contracts.
// Generic interface
public interface Repository<T, ID> {
T findById(ID id);
List<T> findAll();
void save(T entity);
void delete(ID id);
}
// Implementation for User entity
public class UserRepository implements Repository<User, Long> {
@Override
public User findById(Long id) {
// Implementation
return new User(id);
}
@Override
public List<User> findAll() {
// Implementation
return new ArrayList<>();
}
@Override
public void save(User entity) {
// Implementation
}
@Override
public void delete(Long id) {
// Implementation
}
}
Type Parameters and Naming Conventions¶
Java generics follow standard naming conventions for type parameters:
E
- Element (used extensively by the Java Collections Framework)K
- Key (used for mapped types)V
- Value (also used for mapped types)N
- NumberT
- Type (general purpose type)S
,U
,V
etc. - Additional types when multiple type parameters are needed
// Standard conventions in action
public class Container<T> { /* ... */ }
public interface List<E> { /* ... */ }
public interface Map<K, V> { /* ... */ }
public class Converter<S, T> { /* ... */ }
While single-letter names are conventional, you can use more descriptive names when it improves readability:
public class DataProcessor<InputType, OutputType> {
public OutputType process(InputType input) {
// Implementation
return null;
}
}
Type Parameter Bounds¶
Type parameter bounds limit the types that can be used as type arguments in a generic class or method.
Upper Bounds¶
Restricts the type parameter to a specific type or a subtype of that type.
// T must be a Number or a subclass of Number
public class NumberBox<T extends Number> {
private T value;
public NumberBox(T value) {
this.value = value;
}
public double doubleValue() {
return value.doubleValue(); // Can call Number methods
}
public T getValue() {
return value;
}
}
// Usage
NumberBox<Integer> intBox = new NumberBox<>(42);
NumberBox<Double> doubleBox = new NumberBox<>(3.14);
// NumberBox<String> stringBox = new NumberBox<>("not allowed"); // Compile error
// Access to Number methods
double doubleValue = intBox.doubleValue(); // 42.0
Multiple Bounds¶
A type parameter can have multiple bounds, one class and any number of interfaces.
// T must extend Comparable<T> and implement Serializable
public class SortableBox<T extends Comparable<T> & Serializable> {
private T value;
public SortableBox(T value) {
this.value = value;
}
public int compareTo(SortableBox<T> other) {
return this.value.compareTo(other.value);
}
public void save() {
// Can serialize this.value
}
}
// Usage with String (both Comparable and Serializable)
SortableBox<String> box1 = new SortableBox<>("apple");
SortableBox<String> box2 = new SortableBox<>("banana");
int result = box1.compareTo(box2); // -1 (apple < banana)
Generic Methods¶
Generic methods allow type parameters to be scoped to the method level rather than the class level.
Basic Generic Method¶
public class Utils {
// Generic method - type parameter T is defined at method level
public static <T> T identity(T value) {
return value;
}
}
// Usage - explicit type specification (often not needed due to type inference)
String str = Utils.<String>identity("Hello");
// Usage - with type inference
Integer num = Utils.identity(42); // Type inferred as Integer
Type Inference in Generic Methods¶
Java's type inference system allows the compiler to determine the type arguments in many cases.
public class Collections {
public static <T> List<T> emptyList() {
return new ArrayList<>();
}
}
// Types inferred by the context
List<String> strings = Collections.emptyList(); // Inferred as List<String>
Generic Methods with Bounded Type Parameters¶
public class MathUtils {
// Generic method with bounded type parameter
public static <T extends Number> double sum(List<T> numbers) {
double total = 0;
for (T number : numbers) {
total += number.doubleValue(); // Can call Number methods
}
return total;
}
}
// Usage
List<Integer> integers = Arrays.asList(1, 2, 3);
double sum1 = MathUtils.sum(integers); // 6.0
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
double sum2 = MathUtils.sum(doubles); // 6.6
Generic Static Methods¶
Type parameters in static methods are independent of any type parameters in the containing class.
public class Container<T> {
private T value;
// Instance method using the class's type parameter T
public void setValue(T value) {
this.value = value;
}
// Static method with its own type parameter E (independent of T)
public static <E> List<E> asList(E... elements) {
List<E> list = new ArrayList<>();
for (E element : elements) {
list.add(element);
}
return list;
}
}
// Usage of static method (independent of class type parameter)
List<String> strings = Container.asList("a", "b", "c");
List<Integer> numbers = Container.asList(1, 2, 3);
Wildcards¶
Wildcards provide flexibility when working with generic types, especially in method parameters.
Unbounded Wildcards¶
The unbounded wildcard <?>
represents an unknown type. It's useful when you want to work with objects of unknown type.
// Method that prints any type of list
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
// Usage
List<String> strings = Arrays.asList("one", "two", "three");
List<Integer> integers = Arrays.asList(1, 2, 3);
printList(strings); // Works with strings
printList(integers); // Works with integers
Upper Bounded Wildcards¶
Upper bounded wildcards <? extends Type>
allow you to work with a specific type or any of its subtypes.
// Method that sums any list of numbers
public static double sumOfNumbers(List<? extends Number> numbers) {
double sum = 0.0;
for (Number number : numbers) {
sum += number.doubleValue();
}
return sum;
}
// Usage
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
double sum1 = sumOfNumbers(integers); // Works with integers
double sum2 = sumOfNumbers(doubles); // Works with doubles
Lower Bounded Wildcards¶
Lower bounded wildcards <? super Type>
allow you to work with a specific type or any of its supertypes.
// Method that adds integers to a list of integers or any supertype
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// Usage
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
addIntegers(integers); // Works with Integer
addIntegers(numbers); // Works with Number (supertype of Integer)
addIntegers(objects); // Works with Object (supertype of Integer)
PECS (Producer Extends, Consumer Super)¶
A useful mnemonic for wildcards:
- Use <? extends T>
when you need to get values out of a structure (Producer)
- Use <? super T>
when you need to put values into a structure (Consumer)
// Producer (get values) - "extends"
public static void printFirstElement(List<? extends Number> list) {
Number first = list.get(0); // Safe to read as Number
System.out.println(first);
}
// Consumer (add values) - "super"
public static void addElements(List<? super Integer> list) {
list.add(1); // Safe to add Integers
list.add(2);
}
// Both producer and consumer - use specific type
public static void transferElements(List<Integer> source, List<? super Integer> dest) {
for (Integer item : source) {
dest.add(item);
}
}
Type Erasure¶
Java implements generics using type erasure: generic type information is present only at compile time and erased at runtime.
How Type Erasure Works¶
- Replaces type parameters with their bounds or Object if unbounded
- Inserts casts where necessary
- Generates bridge methods to preserve polymorphism
// Before erasure
public class Box<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
// After erasure (conceptual representation)
public class Box {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
Consequences of Type Erasure¶
Cannot Instantiate Type Parameters¶
public class Creator<T> {
// Error: Cannot instantiate the type parameter T
public T create() {
return new T(); // Compiler error
}
// Workaround: Use a factory or Class<T> parameter
public T createWithClass(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
}
Cannot Create Arrays of Parameterized Types¶
// Error: Cannot create arrays of generic types
// List<String>[] array = new List<String>[10]; // Compiler error
// Workaround: Create array of raw type and cast
@SuppressWarnings("unchecked")
List<String>[] array = (List<String>[]) new List[10]; // Works but with unchecked warning
Cannot Use Primitives as Type Arguments¶
// Error: Cannot use primitive types as type arguments
// Box<int> intBox = new Box<>(); // Compiler error
// Solution: Use wrapper classes
Box<Integer> integerBox = new Box<>();
Cannot Overload Methods That Differ Only in Type Parameters¶
public class Processor {
// Compile error: Erasure causes method clash
public void process(List<String> strings) { /* ... */ }
public void process(List<Integer> integers) { /* ... */ }
}
Generics and Collections¶
The Java Collections Framework is one of the primary use cases for generics.
Type-Safe Collections¶
// Type-safe collections
List<String> strings = new ArrayList<>();
strings.add("Hello");
// strings.add(42); // Compile error: incompatible types
// Type-safe map
Map<String, Integer> nameToAge = new HashMap<>();
nameToAge.put("Alice", 30);
nameToAge.put("Bob", 25);
// Type-safe iteration
for (String name : nameToAge.keySet()) {
Integer age = nameToAge.get(name);
System.out.println(name + " is " + age + " years old");
}
Using Collections with Generic Methods¶
public class CollectionUtils {
// Find maximum element in a collection
public static <T extends Comparable<T>> T findMax(Collection<T> collection) {
if (collection.isEmpty()) {
throw new IllegalArgumentException("Collection cannot be empty");
}
Iterator<T> iterator = collection.iterator();
T max = iterator.next();
while (iterator.hasNext()) {
T current = iterator.next();
if (current.compareTo(max) > 0) {
max = current;
}
}
return max;
}
}
// Usage
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9);
Integer max = CollectionUtils.findMax(numbers); // 9
Using Custom Objects with Generics and Collections¶
// Custom class implementing Comparable
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 Integer.compare(this.age, other.age);
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
// Usage in collections
List<Person> people = Arrays.asList(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 35)
);
// Sort list of people (uses Person's compareTo method)
Collections.sort(people);
// Find oldest person using generic method
Person oldest = CollectionUtils.findMax(people); // Charlie (35)
Generics and Inheritance¶
Generics interact with inheritance in ways that can be initially confusing.
Type Relationships¶
For any types A and B where B is a subtype of A:
- B
is a subtype of A
- List<B>
is NOT a subtype of List<A>
- Box<B>
is NOT a subtype of Box<A>
This is a key point that's often surprising to newcomers:
// Regular inheritance
Object obj = "hello"; // String is a subtype of Object
// But with generics:
List<String> strings = new ArrayList<>();
// List<Object> objects = strings; // Compile error!
To understand why this restriction exists, consider what would happen if it were allowed:
// If this were allowed (it's not):
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // Hypothetically allowed
objects.add(42); // Would add an Integer to a List<String>!
String s = strings.get(0); // ClassCastException at runtime
Wildcards for Inheritance Relationships¶
Wildcards provide the solution:
// Using wildcards to enable inheritance relationships
List<String> strings = new ArrayList<>();
strings.add("hello");
// Reading with upper bounded wildcard
List<? extends Object> objects = strings; // OK
Object obj = objects.get(0); // OK to read
// objects.add("world"); // Compile error - can't add to ? extends Object
// Writing with lower bounded wildcard
List<Object> objectList = new ArrayList<>();
List<? super String> stringSuperList = objectList; // OK
stringSuperList.add("hello"); // OK to add String
// String s = stringSuperList.get(0); // Compile error - not safe to read as String
Covariance and Contravariance¶
- Covariance:
<? extends T>
- preserves the "is-a" relationship - Contravariance:
<? super T>
- reverses the "is-a" relationship - Invariance:
<T>
- no subtyping relationship
These terms describe how type relationships are preserved or modified by generics:
// Covariance: can read as T or its supertype
List<? extends Number> numbers = new ArrayList<Integer>();
Number n = numbers.get(0); // Safe, because any element is at least a Number
// numbers.add(1); // Error, can't add to a list with unknown specific type
// Contravariance: can write T or its subtypes
List<? super Number> superNumbers = new ArrayList<Object>();
superNumbers.add(1); // Safe, because any Number can be added
superNumbers.add(1.0); // Safe, because any Number can be added
// Number n = superNumbers.get(0); // Error, can't read because it might be any supertype of Number
Raw Types and Backward Compatibility¶
Raw Types¶
Raw types are generic types without type parameters. They exist for backward compatibility with pre-generic code.
// Raw type (avoid in new code)
List rawList = new ArrayList();
rawList.add("string");
rawList.add(42);
// To retrieve elements, casting is required
String s = (String) rawList.get(0); // May throw ClassCastException at runtime
Working with Legacy Code¶
Sometimes you need to interact with legacy code that uses raw types:
// Legacy method using raw types
public void legacyMethod(List list) {
list.add("Legacy element");
}
// Modern code with generics
List<Integer> integers = new ArrayList<>();
integers.add(1);
integers.add(2);
// Unchecked warning when passing generic to raw type
legacyMethod(integers); // Warning: unchecked call to legacyMethod(List)
// This could cause problems later
// Integer i = integers.get(2); // ClassCastException (String cannot be cast to Integer)
SuppressWarnings Annotation¶
When integrating with legacy code, you can suppress unchecked warnings where appropriate:
public class LegacyIntegration {
// Suppress warnings for a specific statement
@SuppressWarnings("unchecked")
public static <T> List<T> createFromLegacy(List legacyList) {
return (List<T>) new ArrayList<>(legacyList);
}
// Suppress warnings for an entire method
@SuppressWarnings("unchecked")
public static void processList(List list) {
// Operations that would normally cause unchecked warnings
}
}
Advanced Generic Patterns¶
Bounded Type Parameters with Recursive Generics¶
This pattern is used to create self-referential types:
// Recursive type parameter for comparable entities
public abstract class Entity<T extends Entity<T>> implements Comparable<T> {
private Long id;
private String name;
// Default comparison based on ID
@Override
public int compareTo(T other) {
return this.id.compareTo(other.id);
}
}
// Concrete implementation
public class User extends Entity<User> {
private String email;
// User-specific implementation
@Override
public int compareTo(User other) {
// Custom comparison logic (e.g., by email)
return this.email.compareTo(other.email);
}
}
Builder Pattern with Generics¶
Generics enable fluent builder patterns with method chaining:
// Generic builder pattern
public class GenericBuilder<T> {
private final Supplier<T> instantiator;
private final List<Consumer<T>> modifiers = new ArrayList<>();
public GenericBuilder(Supplier<T> instantiator) {
this.instantiator = instantiator;
}
public <V> GenericBuilder<T> with(BiConsumer<T, V> consumer, V value) {
modifiers.add(instance -> consumer.accept(instance, value));
return this;
}
public T build() {
T instance = instantiator.get();
modifiers.forEach(modifier -> modifier.accept(instance));
return instance;
}
}
// Usage
class Person {
private String name;
private int age;
public void setName(String name) { this.name = name; }
public void setAge(int age) { this.age = age; }
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
// Building a person with the generic builder
Person person = new GenericBuilder<>(Person::new)
.with(Person::setName, "John")
.with(Person::setAge, 30)
.build();
Type Safe Heterogeneous Container¶
A technique for storing objects of different types in a single container:
// Type token class
class TypeToken<T> {
private final Class<T> type;
@SuppressWarnings("unchecked")
private TypeToken() {
// Use reflection to get the actual type argument
Type superclass = getClass().getGenericSuperclass();
ParameterizedType paramType = (ParameterizedType) superclass;
this.type = (Class<T>) paramType.getActualTypeArguments()[0];
}
public Class<T> getType() {
return type;
}
}
// Heterogeneous container
class TypeSafeMap {
private final Map<Class<?>, Object> map = new HashMap<>();
public <T> void put(Class<T> type, T instance) {
map.put(type, instance);
}
@SuppressWarnings("unchecked")
public <T> T get(Class<T> type) {
return (T) map.get(type);
}
}
// Usage
TypeSafeMap container = new TypeSafeMap();
container.put(String.class, "Hello");
container.put(Integer.class, 42);
String s = container.get(String.class); // "Hello"
Integer i = container.get(Integer.class); // 42
Best Practices¶
-
Use generics for type safety:
// Avoid raw types in new code List<String> strings = new ArrayList<>(); // Good List rawList = new ArrayList(); // Avoid
-
Favor bounded wildcards for API flexibility:
// Good - allows reading from any list of numbers public double sumOfList(List<? extends Number> list) { /* ... */ } // Good - allows adding integers to any suitable list public void addNumbers(List<? super Integer> list) { /* ... */ }
-
Remember PECS: Producer-Extends, Consumer-Super:
// Producer - use "extends" when getting values public void printElements(Collection<? extends Number> numbers) { for (Number n : numbers) { System.out.println(n); } } // Consumer - use "super" when adding values public void fillWithIntegers(Collection<? super Integer> collection) { collection.add(1); collection.add(2); }
-
Minimize wildcard usage within a class:
// Only use wildcards in public APIs when needed // For private or internal methods, use concrete type parameters
-
Provide factory methods for generic instance creation:
// Factory method to overcome "new T()" restriction public static <T> List<T> createArrayList(Class<T> clazz) { return new ArrayList<>(); }
-
Use explicit type parameters when type inference fails:
// When inference doesn't work List<String> list = Collections.<String>emptyList();
-
Document generic parameters clearly:
/** * Performs a binary search on the specified list. * * @param <T> the type of elements in the list * @param list the list to be searched (must be sorted) * @param key the key to be searched for * @return the index of the key, if it is contained in the list; * otherwise, (-(insertion point) - 1) */ public static <T extends Comparable<? super T>> int binarySearch(List<? extends T> list, T key) { // Implementation }
-
Use generic types all the way through:
// Maintain type safety throughout your code public <T> List<T> filterList(List<T> list, Predicate<T> predicate) { List<T> result = new ArrayList<>(); for (T element : list) { if (predicate.test(element)) { result.add(element); } } return result; }
-
Use @SuppressWarnings sparingly and with comments:
// Only suppress warnings when you're sure it's safe @SuppressWarnings("unchecked") // Safe because we know the list contains only strings public static <T> List<T> createList(T... elements) { return (List<T>) Arrays.asList(elements); }
-
Avoid excessive generic complexity:
// Overly complex generics can be hard to understand public <K, V extends Comparable<? super V>> Pair<K, V> findMaxByValue(Map<K, V> map) { // This is already complex enough } // Even more complex - avoid unless necessary public <T, S extends Collection<? extends T>, R extends Collection<? super T>> R transferElements(S source, R dest) { // Too complex }
Common Pitfalls and How to Avoid Them¶
-
Mixing raw types with generics:
// Problematic List rawList = new ArrayList<String>(); // Raw use of List rawList.add(42); // No compile-time error, but will cause problems // Correct List<String> stringList = new ArrayList<>();
-
Trying to instantiate type parameters:
// Won't work public <T> T create() { return new T(); // Compiler error } // Alternative: pass a factory or Class<T> with newInstance() public <T> T create(Supplier<T> factory) { return factory.get(); }
-
Attempting to create arrays of parameterized types:
// Won't work List<String>[] arrayOfLists = new List<String>[10]; // Compiler error // Alternative: use a List of Lists List<List<String>> listOfLists = new ArrayList<>(); for (int i = 0; i < 10; i++) { listOfLists.add(new ArrayList<>()); }
-
Overloading methods with different generic parameters:
// Won't work - after erasure, these are the same method public void process(List<String> strings) { /* ... */ } public void process(List<Integer> integers) { /* ... */ } // Alternative: use different method names public void processStrings(List<String> strings) { /* ... */ } public void processIntegers(List<Integer> integers) { /* ... */ }
-
Assuming List
is a subtype of List :// Won't work List<Dog> dogs = new ArrayList<>(); List<Animal> animals = dogs; // Compiler error // Use wildcards instead List<? extends Animal> animals = dogs; // OK
-
Ignoring compiler warnings:
// Unchecked assignment warnings should not be ignored without thought List<String> strings = new ArrayList(); // Warning: unchecked conversion // Either fix the code or suppress with justification @SuppressWarnings("unchecked") // Suppression reason explained here List<String> strings = new ArrayList();
-
Excessive use of wildcards:
// Too complex Map<? extends String, ? extends List<? extends Number>> map; // Consider simplifying Map<String, List<Number>> map;
-
Forgetting generic type arguments:
// Accidental use of raw type Set set = new HashSet<>(); // Missing type arguments set.add("string"); Integer i = (Integer) set.iterator().next(); // ClassCastException // Be explicit Set<String> set = new HashSet<>();
-
Combining wildcards with type inference incorrectly:
// This won't compile as expected List<?> wildcardList = new ArrayList<>(); wildcardList.add("string"); // Compiler error - can't add to List<?> // Correct usage List<String> typedList = new ArrayList<>(); typedList.add("string"); List<?> wildcardList = typedList; // OK for read-only operations
-
Misunderstanding invariance:
// Won't work List<Object> objectList = new ArrayList<String>(); // Compiler error // Won't work either void addToList(List<Object> list) { list.add(42); } List<String> strings = new ArrayList<>(); addToList(strings); // Compiler error, prevents heap pollution
Resources for Further Learning¶
- Official Documentation:
- Java Generics Tutorial
-
Books:
- "Java Generics and Collections" by Maurice Naftalin and Philip Wadler
- "Effective Java" by Joshua Bloch (Chapter on Generics)
-
"Java Generics" by Gilad Bracha
-
Online Resources:
- Baeldung Java Generics Tutorials
- Oracle's Java Magazine: Understanding Generics
-
Advanced Topics:
- Type Erasure Details
- Wildcards in Java
- Reifiable Types
Practice Exercises¶
-
Generic Box Implementation: Create a generic
Box<T>
class that can store and retrieve a value of any type. Implement methods to check if the box is empty and to clear its contents. -
Generic Pair Class: Implement a
Pair<K,V>
class that holds two values of potentially different types. Include methods to access, modify, and swap the values. -
Generic Stack: Create a generic stack implementation with
push()
,pop()
,peek()
, andisEmpty()
methods. -
Type-Safe Heterogeneous Container: Implement a container that can store objects of different types and retrieve them safely without casting.
-
Generic Binary Tree: Implement a generic binary tree structure with methods for insertion, traversal, and search.
-
Function Composition: Create a utility class that allows composing functions with different input and output types using generics.
-
Generic Sorting: Implement a generic method that can sort any list of comparable objects.
-
Generic Cache: Create a cache implementation that can store and retrieve different types of objects using keys.
-
Type-Safe Builder Pattern: Implement a generic builder pattern that ensures type safety during the building process.
-
Covariant Result Type Pattern: Create a class hierarchy with methods that return a more specific type in subclasses using generics.