🧬 Chapter 11: Java Generics – Type-Safe Reusability and Mechanics
## 1. Why Generics Exist: The Pre-Java 5 Reality
To understand why generics are essential, consider how Java worked before version 1.4. In those early days, general-purpose containers held plain `Object` references:
```java
// Pre-Java 5 approach
class LegacyBox {
private Object content;
public void set(Object content) {
this.content = content;
}
public Object get() {
return content;
}
}
```
This design had two major flaws:
1. **Tedious Casting:** Every time you extracted a value, you had to cast it back to its specific type:
```java
LegacyBox box = new LegacyBox();
box.set("Hello World");
String text = (String) box.get(); // Explicit cast required
```
2. **Runtime Fragility:** The compiler could not catch type mismatches. If someone placed an integer into that box, compilation succeeded without issue:
```java
box.set(42);
String result = (String) box.get(); // Compiles fine, throws ClassCastException at runtime!
```
A `ClassCastException` in production is one of the most frustrating errors to debug. Generics shift this validation from **runtime to compile-time**. Errors are flagged directly in your editor before the code ever executes.
---
## 2. Generic Classes and Interfaces
Creating a generic class requires declaring a **formal type parameter** within angle brackets `<>` right after the class name.
### Standard Type Parameter Naming Conventions
By industry convention, type parameter names are single, uppercase letters:
* `E` - Element (used heavily by the Java Collections Framework)
* `K` - Key
* `V` - Value
* `N` - Number
* `T` - Type (first general type parameter)
* `U`, `S` - Second and third general type parameters
### Building a Generic Container
```java
public class Box<T> {
private T item;
public void set(T item) {
this.item = item;
}
public T get() {
return item;
}
public boolean isEmpty() {
return item == null;
}
}
```
Now, the caller specifies the actual type argument during instantiation:
```java
public class Main {
public static void main(String[] args) {
// Box dedicated exclusively to Strings
Box<String> quoteBox = new Box<>();
quoteBox.set("Programs must be written for people to read.");
String quote = quoteBox.get(); // No cast needed!
// Box dedicated exclusively to custom domain entities
Box<Integer> numberBox = new Box<>();
numberBox.set(1024);
// numberBox.set("Invalid"); // Compiler rejects this immediately
}
}
```
### Generic Interfaces
Interfaces can also be parameterized. A classic example is a data access repository pattern:
```java
public interface Repository<T, ID> {
void save(T entity);
T findById(ID id);
void delete(ID id);
}
// Implementing class binds concrete types:
public class UserRepository implements Repository<User, Long> {
@Override
public void save(User entity) { /* save logic */ }
@Override
public User findById(Long id) { /* retrieval logic */ return null; }
@Override
public void delete(Long id) { /* delete logic */ }
}
```
---
## 3. Generic Methods
You do not need a generic class to write a generic method. Individual methods can declare their own type parameters before the return type:
```java
public class ArrayUtilities {
// Generic method to swap elements in an array of any reference type
public static <E> void swap(E[] array, int firstIndex, int secondIndex) {
E temp = array[firstIndex];
array[firstIndex] = array[secondIndex];
array[secondIndex] = temp;
}
public static void main(String[] args) {
String[] words = {"world", "hello"};
ArrayUtilities.swap(words, 0, 1);
System.out.println(words[0] + " " + words[1]); // Prints "hello world"
Integer[] digits = {10, 20, 30};
ArrayUtilities.swap(digits, 0, 2); // Reuses the exact same method
}
}
```
---
## 4. Bounded Type Parameters: Constraining Types
Sometimes accepting *any* type `T` is too broad. Suppose you want to write a calculation engine that operates only on numbers. If `T` can be a `String` or a `Scanner`, mathematical operations won't work.
You can restrict allowed types using the `extends` keyword (known as an **upper bound**):
```java
// T must be Number or a subclass of Number (Integer, Double, BigDecimal, etc.)
public class NumericStats<T extends Number> {
private final T[] numbers;
public NumericStats(T[] numbers) {
this.numbers = numbers;
}
public double calculateAverage() {
double sum = 0.0;
for (T num : numbers) {
// Because T extends Number, we can safely call doubleValue()
sum += num.doubleValue();
}
return sum / numbers.length;
}
}
```
### Multiple Bounds
A type parameter can be constrained by a class and one or more interfaces using the `&` operator:
```java
// T must extend Animal AND implement both Comparable and Serializable
public class ZooCage<T extends Animal & Comparable<T> & java.io.Serializable> {
// Implementation
}
```
*(Note: If a concrete class is part of the bound, it must appear first, followed by interfaces).*
---
## 5. Subtyping and Wildcards
Here is a common conceptual stumbling block for developers:
> Is `List<Integer>` a subtype of `List<Number>`?
Intuitively, since `Integer` extends `Number`, you might expect `List<Integer>` to be assignable to `List<Number>`. In Java, **it is not**. Generics are **invariant**:
```java
List<Integer> intList = new ArrayList<>();
// List<Number> numList = intList; // COMPILE ERROR!
```
If this assignment were allowed, you could write:
```java
numList.add(3.14); // Legal for List<Number>, but invalid for the underlying List<Integer>!
Integer item = intList.get(0); // Crashes with ClassCastException
```
To enable polymorphism with generics, Java uses **Wildcards**, denoted by the question mark `?`.
---
## 6. The PECS Principle: Producer `extends`, Consumer `super`
Wildcards come in three forms, each designed for specific access patterns.
### A. Upper Bounded Wildcards (`? extends T`)
Use this when your method reads data from a structure:
```java
// Accepts a list containing Number or any subtype of Number
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list) {
s += n.doubleValue(); // Safe to read as Number
}
return s;
}
```
*Restriction:* You **cannot write** to a `List<? extends Number>` (except `null`), because the compiler cannot guarantee which specific subtype the list actually holds.
### B. Lower Bounded Wildcards (`? super T`)
Use this when your method writes data into a structure:
```java
// Accepts a list containing Integer or any supertype of Integer (e.g., Number, Object)
public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3); // Safe to write an Integer
}
```
*Restriction:* When you read from this list, the return type is always degraded to `Object`, since you don't know the exact ancestor class.
### C. Unbounded Wildcards (`?`)
Used when you only care about functionality provided by the base `Object` class, or when code doesn't depend on the type parameter:
```java
public static void printList(List<?> list) {
for (Object elem : list) {
System.out.println(elem);
}
}
```
### Memorizing PECS:
* **P**roducer $\rightarrow$ Use **`extends`** (the collection *produces* data for you to read).
* **C**onsumer $\rightarrow$ Use **`super`** (the collection *consumes* data you pass into it).
---
## 7. Under the Hood: Type Erasure
How does the JVM handle generics while remaining backwards-compatible with code written in the 1990s? Through a process called **Type Erasure**.
Generics exist primarily for the compiler. Once compilation finishes, the compiler:
1. Replaces all type parameters with their bounds (or `Object` if unbounded).
2. Inserts appropriate casts automatically to preserve type safety.
3. Generates synthetic bridge methods to preserve polymorphism in extended classes.
4. Strips away the generic type metadata from runtime class bytecode.
### What You Write vs. What the Bytecode Runs:
```java
// Your code
public class Node<T> {
private T data;
public T getData() { return data; }
}
// What the compiler outputs after type erasure
public class Node {
private Object data;
public Object getData() { return data; }
}
```
If bounded:
```java
// Your code
public class NumberNode<T extends Number> {
private T data;
}
// After type erasure
public class NumberNode {
private Number data;
}
```
### Direct Consequences of Type Erasure:
Because types vanish at runtime, certain operations are forbidden:
1. **Cannot use primitives directly as type arguments:**
```java
List<int> list; // Invalid -> Use List<Integer>
```
2. **Cannot instantiate type parameters:**
```java
T obj = new T(); // Compile error: 'T' is not a concrete constructor at runtime
```
3. **Cannot create arrays of parameterized types:**
```java
T[] array = new T[10]; // Compile error
```
4. **Cannot check generic types with `instanceof`:**
```java
if (obj instanceof List<String>) // Compile error: JVM can only check 'instanceof List<?>'
```
5. **Static fields cannot reference class type parameters:**
```java
private static T sharedItem; // Compile error: static context is shared across all instances
```
---
## 8. Practical Example: Building an In-Memory Object Cache
Let’s combine custom generic classes, upper bounds, and interfaces into a practical component: a thread-safe cache entry holder with an expiration policy.
```java
import java.time.Instant;
// Marker interface for entities that provide unique identification
interface Identifiable<ID> {
ID getId();
}
// Generic Cache Entry wrapping any Identifiable object
public class CacheEntry<ID, T extends Identifiable<ID>> {
private final T payload;
private final Instant expiresAt;
public CacheEntry(T payload, long ttlSeconds) {
this.payload = payload;
this.expiresAt = Instant.now().plusSeconds(ttlSeconds);
}
public boolean isExpired() {
return Instant.now().isAfter(expiresAt);
}
public T getPayload() {
if (isExpired()) {
return null;
}
return payload;
}
public ID getKey() {
return payload.getId();
}
}
```
### Implementing a Domain Class and Testing the Cache:
```java
class CustomerAccount implements Identifiable<String> {
private final String accountId;
private final String holderName;
public CustomerAccount(String accountId, String holderName) {
this.accountId = accountId;
this.holderName = holderName;
}
@Override
public String getId() {
return accountId;
}
@Override
public String toString() {
return "CustomerAccount{" + "id='" + accountId + "', name='" + holderName + "'}";
}
}
public class CacheTest {
public static void main(String[] args) {
CustomerAccount account = new CustomerAccount("ACC-9081", "Jane Doe");
// Fully type-checked cache entry
CacheEntry<String, CustomerAccount> entry = new CacheEntry<>(account, 60);
System.out.println("Cached ID: " + entry.getKey());
System.out.println("Data: " + entry.getPayload());
System.out.println("Is expired? " + entry.isExpired());
}
}
```
---
## 📝 Chapter Summary
* **Generics** eliminate manual type casting and catch errors at compile-time instead of runtime.
* Parameterize classes with `<T>`, interfaces with `<T, ID>`, and individual methods with `<E>`.
* Use **Bounded Type Parameters** (`<T Number extends>`) to restrict inputs to specific types and expose their methods safely.
* Remember that generics are **invariant**: `List<Integer>` is not a `List<Number>`.
* Follow the **PECS rule**: Use `? extends T` when reading (producing) values, and `? super T` when writing (consuming) values.
* Understand **Type Erasure**: The JVM strips type parameter metadata at runtime for backward compatibility, which explains restrictions on `new T()` and primitive types.
Now that our data structures are flexible, robust, and type-safe, we are ready to move toward modern functional idioms. In modern Java, we don't just store data—we process it efficiently and concisely.when reading (producing) values, and `? super T` when writing (consuming) values.
* Understand **Type Erasure**: The JVM strips type parameter metadata at runtime for backward compatibility, which explains restrictions on `new T()` and primitive types.
Now that our data structures are flexible, robust, and type-safe, we are ready to move toward modern functional idioms. In modern Java, we don't just store data—we process it efficiently and concisely.