Generics
Type parameters and generic programming
1. Introduction to Generics
Generics enable types (classes and interfaces) to be parameters when defining classes, interfaces, and methods. They provide stronger type checking at compile time.
Benefits of Generics
- Compile-time type checking
- Elimination of casts
- Generic algorithms
- Code reusability
Generic Syntax
- <T> - Type parameter
- <E> - Element type
- <K,V> - Key, Value types
- <?> - Wildcard
Before Generics (Raw Types)
// Without generics - requires casting and runtime checks
List list = new ArrayList();
list.add("Hello");
list.add(123); // Different types mixed!
String str = (String) list.get(0); // Casting required
Integer num = (Integer) list.get(1); // Runtime error possible
With Generics (Type Safe)
// With generics - compile-time type safety
List<String> stringList = new ArrayList<>();
stringList.add("Hello");
// stringList.add(123); // Compile-time error!
String str = stringList.get(0); // No casting needed
// Type safety guaranteed at compile time
2. Type Safety & Generic Classes
Generics provide type safety by enabling you to detect errors at compile time rather than runtime.
Generic Class Example
// Generic class with type parameter T
public class Box<T> {
private T content;
public void set(T content) {
this.content = content;
}
public T get() {
return content;
}
public boolean isEmpty() {
return content == null;
}
}
// Usage
Box<String> stringBox = new Box<>();
stringBox.set("Hello World");
String value = stringBox.get(); // No casting needed
Box<Integer> intBox = new Box<>();
intBox.set(42);
Integer number = intBox.get();
Generic Methods
public class Utility {
// Generic method with type parameter
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// Generic method with return type
public static <T> T getMiddleElement(T[] array) {
return array[array.length / 2];
}
// Multiple type parameters
public static <K, V> void printKeyValue(K key, V value) {
System.out.println(key + " = " + value);
}
}
// Usage
String[] names = {"Alice", "Bob", "Charlie"};
Utility.swap(names, 0, 2); // Alice and Charlie swapped
Integer[] numbers = {1, 2, 3, 4, 5};
Integer middle = Utility.getMiddleElement(numbers); // Returns 3
3. Wildcards
Wildcards (?) represent unknown types in generic code, providing flexibility when exact types are not known.
Unbounded Wildcards
// Unbounded wildcard - accepts any type
public static void printList(List<?> list) {
for (Object item : list) {
System.out.println(item);
}
}
// Can accept any parameterized List
List<String> stringList = Arrays.asList("A", "B", "C");
List<Integer> intList = Arrays.asList(1, 2, 3);
List<Double> doubleList = Arrays.asList(1.1, 2.2, 3.3);
printList(stringList); // Works
printList(intList); // Works
printList(doubleList); // Works
Upper Bounded Wildcards (extends)
// Upper bounded wildcard - accepts Number and its subtypes
public static double sumNumbers(List<? extends Number> numbers) {
double sum = 0.0;
for (Number num : numbers) {
sum += num.doubleValue();
}
return sum;
}
// Usage with different Number subtypes
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.5, 2.5, 3.5);
List<Float> floats = Arrays.asList(1.0f, 2.0f, 3.0f);
double sum1 = sumNumbers(integers); // Works
double sum2 = sumNumbers(doubles); // Works
double sum3 = sumNumbers(floats); // Works
Lower Bounded Wildcards (super)
// Lower bounded wildcard - accepts Integer and its supertypes
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
// Usage
List<Integer> intList = new ArrayList<>();
List<Number> numberList = new ArrayList<>();
List<Object> objectList = new ArrayList<>();
addNumbers(intList); // Works
addNumbers(numberList); // Works
addNumbers(objectList); // Works
PECS Rule
Producer Extends, Consumer Super - Use extends when you only read from a structure (producer), use super when you only write to a structure (consumer).
4. Bounded Parameters
Bounded type parameters restrict the types that can be used as type arguments, enabling access to methods of the bound type.
Single Bound
// Bounded type parameter - must extend Number
public class NumberBox<T extends Number> {
private T value;
public NumberBox(T value) {
this.value = value;
}
// Can call Number methods because T extends Number
public double getDoubleValue() {
return value.doubleValue();
}
public int getIntValue() {
return value.intValue();
}
// Compare numeric values
public boolean isGreaterThan(NumberBox<? extends Number> other) {
return this.getDoubleValue() > other.getDoubleValue();
}
}
// Usage
NumberBox<Integer> intBox = new NumberBox<>(42);
NumberBox<Double> doubleBox = new NumberBox<>(3.14);
boolean result = intBox.isGreaterThan(doubleBox);
Multiple Bounds
// Interface for comparison
interface Drawable {
void draw();
}
// Class with multiple bounds
public class GraphicElement<T extends Shape & Drawable & Comparable<T>> {
private T element;
public GraphicElement(T element) {
this.element = element;
}
public void render() {
element.draw(); // From Drawable
System.out.println("Area: " + element.calculateArea()); // From Shape
}
public int compare(GraphicElement<T> other) {
return element.compareTo(other.element); // From Comparable
}
}
5. Advanced Topics
Explore complex scenarios including generic inheritance, recursive bounds, and type token patterns.
Recursive Type Bounds
// Recursive bound - T must be comparable to itself
public class SortedList<T extends Comparable<T>> {
private List<T> items = new ArrayList<>();
public void add(T item) {
items.add(item);
Collections.sort(items); // Safe because T implements Comparable<T>
}
public T get(int index) {
return items.get(index);
}
}
// Usage
SortedList<String> stringList = new SortedList<>();
stringList.add("Charlie");
stringList.add("Alice");
stringList.add("Bob");
// Items are automatically sorted: Alice, Bob, Charlie
Generic Builder Pattern
// Generic builder with fluent interface
public class ApiResponse<T> {
private final T data;
private final String message;
private final int statusCode;
private ApiResponse(Builder<T> builder) {
this.data = builder.data;
this.message = builder.message;
this.statusCode = builder.statusCode;
}
public static <T> Builder<T> builder() {
return new Builder<>();
}
public static class Builder<T> {
private T data;
private String message = "Success";
private int statusCode = 200;
public Builder<T> data(T data) {
this.data = data;
return this;
}
public Builder<T> message(String message) {
this.message = message;
return this;
}
public ApiResponse<T> build() {
return new ApiResponse<>(this);
}
}
}
// Usage
ApiResponse<List<User>> response = ApiResponse.<List<User>>builder()
.data(userList)
.message("Users retrieved successfully")
.build();
6. Best Practices
Good Practices Example
// Good: Clear generic interface
public interface Repository<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
T save(T entity);
void deleteById(ID id);
// Using bounded wildcards for flexibility
List<T> findByExample(T example);
void saveAll(Collection<? extends T> entities);
}
// Good: Proper use of bounds
public class NumberProcessor<T extends Number & Comparable<T>> {
public T max(Collection<? extends T> numbers) {
return numbers.stream()
.max(T::compareTo)
.orElse(null);
}
}
// Avoid: Raw types
List rawList = new ArrayList(); // Don't do this
// Prefer: Parameterized types
List<String> stringList = new ArrayList<>(); // Better
7. Summary
Key Takeaways
- Type Safety: Compile-time error detection eliminates casting and runtime errors
- Generic Classes/Methods: Create reusable, type-safe code with type parameters
- Wildcards: Use ? for unknown types, extends for upper bounds, super for lower bounds
- PECS Principle: Producer Extends, Consumer Super - remember this for wildcard usage
- Type Erasure: Generic information removed at runtime for backward compatibility
Type Safety
Compile-time checks, no casting, type parameter constraints
Wildcards
Unbounded (?), upper bounds (extends), lower bounds (super)
Bounded Parameters
Single/multiple bounds, recursive bounds, method access
8. Common Interview Questions
Interview Tips
- Understand PECS: Know when to use extends vs super
- Explain Benefits: Type safety, no casting, compile-time checks
- Code Examples: Be ready to write generic methods
- Know Limitations: Type erasure, array restrictions