Implementing Generics in Java
Robust Typing, Bounded Polymorphism, and Erasure Semantics for Clean, Reusable Code
🚀 Why Generics Matter
Java generics provide a compile-time mechanism to enforce type safety without sacrificing
flexibility. By introducing type parameters, developers can build:
Strongly-typed, reusable data structures
Declarative algorithms (e.g. Collections.sort(List<T>) )
API contracts that are self-documenting and fail early
1/8
Behind the scenes, generics are a syntactic abstraction. Under type erasure, they vanish at
runtime — making understanding their limitations as important as their capabilities.
🔧 1. Generic Classes – Syntax & Structure
Generic classes are declared using angle brackets <> :
public class MyList<T> {
private T[] items;
private int index = 0;
@SuppressWarnings("unchecked")
public MyList(int size) {
items = (T[]) new Object[size]; // type erasure workaround
}
public void add(T item) {
items[index++] = item;
}
public T get(int i) {
return items[i];
}
}
⚠️Key Pitfalls
You cannot instantiate generic arrays directly: new T[10] → ❌
You must explicitly cast when initializing: (T[]) new Object[size]
Without @SuppressWarnings("unchecked") , you'll get compiler warnings due to unsafe
casting
🧭 2. Multiple Type Parameters
Generics can declare multiple type parameters. Each must be distinct and follow type
naming conventions.
public class Pair<K, V> {
private final K key;
private final V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
2/8
public K getKey() { return key; }
public V getValue() { return value; }
}
📌 Use Cases
Pair<K, V> → for mapping keys to values
Map<K, V> → for typed key-value stores
Function<T, R> → for declaring input/output processors
⛓ 3. Bounded Type Parameters
Bounded type parameters limit acceptable types and enable richer semantics.
3.1 Single Bound
public class Calculator<T extends Number> {
public double doubleValue(T input) {
return input.doubleValue();
}
}
3.2 Multiple Bounds
public class ComparableBox<T extends Number & Comparable<T>> {
public boolean isGreaterThan(T a, T b) {
return a.compareTo(b) > 0;
}
}
💡 Rule
You can only extend one class ( Number here), but implement multiple interfaces.
🔄 4. Generic Methods
public class Utils {
public static <T> T getFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
}
3/8
🧠 Notes
The <T> must be declared before the return type
Class generics ≠ method generics: they are scoped independently
Static methods must declare their own type parameters
🌀 5. Wildcards: ? , extends , super
Wildcards allow covariance and contravariance in method arguments.
Concept Relationship to Subtyping Direction of Allowed Type Conversion
Covariance Preserves subtyping (read- Subtype → Supertype allowed (read
only) from)
Contravariance Reverses subtyping (write- Supertype → Subtype allowed (write
only) to)
Invariance No subtyping relation Must be exactly the same type
✅ Covariance – “Producer Extends”
Covariance allows a generic type to accept subtypes of a given type. You read from but
cannot write to it safely.
List<? extends Number> list = new ArrayList<Integer>();
Number n = list.get(0); // ✅ OK
list.add(1); // ❌ Compile error
🧠 Think: "I can treat a
List<Integer> as a List<? extends Number> because I’m only
consuming/reading Numbers."
🔁 Contravariance – “Consumer Super”
Contravariance allows a generic type to accept supertypes. You can write to it, but reading is
only safe as Object .
List<? super Integer> list = new ArrayList<Number>();
list.add(1); // ✅ OK
Object o = list.get(0); // ✅ Only as Object
Integer i = list.get(0); // ❌ Compile error
🧠 Think: "I can add
Integer s to List<? super Integer> — which may be
List<Integer> or List<Number> — but I don't know what I’ll get out."
4/8
❗️ Invariance – Default in Java Generics
Java generics are invariant by default:
List<Object> objects;
List<String> strings = new ArrayList<>();
objects = strings; // ❌ Compile error
Even though String extends Object , List<String> is not a subtype of List<Object> .
🤯 Real-world Analogy
Covariant: You can borrow a book from a Library of Books (read-only) even if it's
specifically a Library of Novels.
Contravariant: You can donate any novel to a Library accepting Books (write-only).
📌 In Java: ? extends vs ? super
Usage Behavior Example
List<? extends T> Covariant (read) Accepts any list of T or its subtypes
List<? super T> Contravariant (write) Accepts any list of T or its supertypes
🔄 PECS Rule (by Joshua Bloch)
"Producer Extends, Consumer Super"
If a parameterized type produces values, use ? extends T
If it consumes values, use ? super T
5.1 Unbounded Wildcard
public void printAll(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}
5.2 Upper Bound – <? extends T> (Covariant)
public double sum(List<? extends Number> list) {
return list.stream().mapToDouble(Number::doubleValue).sum();
5/8
}
→ Can read safely, but cannot write.
5.3 Lower Bound – <? super T> (Contravariant)
public void fillWithIntegers(List<? super Integer> list) {
list.add(42);
}
→ Can write safely, but cannot assume readable type beyond Object .
🧮 6. Type Erasure: Runtime Limitations
Java compiles generics into raw types and erases the parameter types at runtime.
List<String> list = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
System.out.println(list.getClass() == ints.getClass()); // true
Impacts:
Limitation Explanation
No instanceof for generics obj instanceof List<String> → ❌ compile error
No reflection of generic list.getClass().getTypeParameters() → metadata
types only
No generic array creation new T[10] is illegal → use casting workaround
📉 7. Common Pitfalls & Anti-patterns
Pitfall Example Fix
Using raw types List list = new Always parameterize:
ArrayList(); List<String>
Overusing wildcards List<? extends Prefer List<T> when defining
Object> APIs
Type inference surprises <T> T cast(Object Enforce type with <T extends
obj) Foo>
Ineffective bounds T extends Object Redundant — all classes extend
Object
6/8
Pitfall Example Fix
Exposing wildcards in public List<?> Prefer List<T> or use bounded
return types getData() wildcards in args
🔐 8. Generic Interfaces
public interface Converter<S, T> {
T convert(S source);
}
Use in APIs
public class StringToInteger implements Converter<String, Integer> {
public Integer convert(String input) {
return Integer.valueOf(input);
}
}
🏗 9. Design Patterns Using Generics
Pattern Application
Factory Pattern public <T> T create(Class<T> clazz)
Fluent Builder return (T) this with bounded self-referencing generics ( T
extends Builder<T> )
Strategy Pattern interface Validator<T> – replace conditionals with polymorphism
Decorator Processor<T> wrappers that extend behavior via composition
Pattern
✅ 10. Best Practices
✅ Name type params meaningfully: T , E , K , V , R
✅ Minimize wildcard exposure in public APIs
✅ Prefer composition of Function<T, R> over ad hoc custom interfaces
❌ Avoid raw types — type safety is lost
✅ Use Optional<T> instead of nullable generic return types
✅ Use bounded generics ( T extends Comparable<T> ) to enable rich behavior
🧾 11. Summary Table
7/8
Concept Description
Generic Class Parameterized data container ( class Box<T> )
Generic Method Independent type logic ( <T> T identity(T input) )
Bounded Types Constrain T with extends , multiple bounds ( & )
Wildcards ? , ? extends , ? super for flexible API contracts
Type Erasure Type information erased at runtime → no casts, instanceof, arrays
Invariance List<A> ≠ List<B> even if A extends B
Patterns Factory, Builder, Strategy, Adapter with generic parametrization
Reflection Generic type info only partially available via TypeVariable
8/8