Generics in Java are designed to provide type safety, reusability, and maintainability. Among their most powerful features are bounded type parameters, which allow developers to constrain the types that can be used in generic classes or methods. This ensures flexibility without sacrificing safety.
Bounded type parameters rely on the keywords extends
and super
, which define upper bounds and lower bounds. By mastering these, developers can write cleaner, more reusable APIs that adapt to different type hierarchies while enforcing meaningful constraints.
Think of bounded type parameters as blueprint molds with restrictions. While generics are molds that can take any material, bounded type parameters say, “you can only pour materials of a certain type or hierarchy into this mold.”
Core Definition and Purpose of Java Generics
Generics allow classes, interfaces, and methods to operate on type parameters instead of specific types. This provides:
- Type Safety – Prevents invalid types at compile time.
- Reusability – Enables one design to work across many data types.
- Maintainability – Cleaner, easier-to-read APIs.
Introduction to Type Parameters: <T>
, <K, V>
, <E>
<T>
: General type parameter (Type).<E>
: Represents an element, often used in collections.<K, V>
: Used in key-value mappings.
Example:
class Box<T> {
private T content;
public void set(T content) { this.content = content; }
public T get() { return content; }
}
Generic Classes with Examples
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; }
}
Usage:
Pair<String, Integer> age = new Pair<>("Age", 30);
System.out.println(age.getKey() + " = " + age.getValue());
Generic Methods: Declaration and Usage
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.println(element);
}
}
Bounded Type Parameters: extends
and super
Upper Bounds with extends
<T extends Class>
means T can be Class or its subtype.
public static <T extends Number> double square(T number) {
return number.doubleValue() * number.doubleValue();
}
- Ensures only numeric types are allowed.
- Prevents unrelated types like
String
from being passed.
Lower Bounds with super
<? super T>
means the parameter can be of type T
or any of its supertypes.
public static void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
- Ensures consumers can safely add items without breaking type safety.
Wildcards Explained: ?
, ? extends
, ? super
?
– Unknown type.? extends T
– Upper bound (read-only, producer).? super T
– Lower bound (write-only, consumer).
PECS Principle: Producer Extends, Consumer Super.
public static void processNumbers(List<? extends Number> list) { // producer
for (Number n : list) {
System.out.println(n);
}
}
public static void insertNumbers(List<? super Integer> list) { // consumer
list.add(100);
}
Multiple Type Parameters and Nested Generics
class Triple<A, B, C> {
private A first; private B second; private C third;
}
Example: Map<String, List<Integer>>
.
Generics in Collections Framework
List<E>
→List<String>
Map<K, V>
→Map<String, Integer>
Set<E>
→Set<Double>
Collections heavily use bounded type parameters.
Raw Types vs Parameterized Types
List list = new ArrayList(); // Raw type - unsafe
List<String> safe = new ArrayList<>(); // Parameterized type
Raw types bypass type checks and should be avoided.
Type Inference and Diamond Operator
Introduced in Java 7:
Map<String, Integer> map = new HashMap<>();
Type Erasure in Java
Generics are implemented with type erasure.
At runtime, type parameters are erased to their bounds (or Object
).
This explains why you cannot create new T()
.
Reifiable vs Non-Reifiable Types
- Reifiable Types – Fully known at runtime (
List<?>
). - Non-Reifiable Types – Lose type info after erasure (
List<String>
).
Recursive Type Bounds
class ComparableBox<T extends Comparable<T>> { ... }
Useful for defining self-referential hierarchies.
Fluent APIs and Builders with Generics
class Builder<T extends Builder<T>> {
public T withName(String name) { return (T) this; }
}
Generics with Enums and Annotations
Example: EnumSet<E extends Enum<E>>
.
Annotations like @SuppressWarnings("unchecked")
apply to generics.
Generics with Exceptions
- Cannot declare
catch (T e)
. - Cannot instantiate
new T()
.
Generics and Reflection
Reflection can retrieve generic info via ParameterizedType
.
Case Studies
Type-Safe Cache
class Cache<K, V> {
private Map<K, V> map = new HashMap<>();
public void put(K key, V value) { map.put(key, value); }
public V get(K key) { return map.get(key); }
}
Flexible Repository Pattern
interface Repository<T, ID> {
void save(T entity);
T findById(ID id);
}
Event Handling System
interface EventListener<T> {
void onEvent(T event);
}
Best Practices for Bounded Type Parameters
- Use upper bounds (
extends
) for read-only scenarios. - Use lower bounds (
super
) for write-only scenarios. - Follow PECS strictly.
- Avoid unnecessary wildcards.
Common Anti-Patterns
- Overly complex nested generics.
- Misuse of wildcards.
- Using raw types.
Performance Considerations
Bounded type parameters do not affect runtime performance since generics use type erasure. Their power lies in compile-time safety.
📌 What's New in Java for Generics?
- Java 5: Generics introduced.
- Java 7: Diamond operator (
<>
). - Java 8: Streams and functional interfaces use generics.
- Java 10:
var
integrates with generics. - Java 17+: Sealed classes interact with generics.
- Java 21: Virtual threads make concurrent generic APIs more scalable.
Conclusion and Key Takeaways
Bounded type parameters (extends
and super
) give developers fine-grained control over how generics interact with type hierarchies. By combining them with wildcards and following the PECS principle, you can design APIs that are flexible, reusable, and safe.
FAQ on Bounded Type Parameters in Java
Q1: What’s the difference between extends
and super
in generics?extends
sets an upper bound (subtypes allowed), super
sets a lower bound (supertypes allowed).
Q2: Why can’t I create new T()
in a generic class?
Because type information is erased at runtime.
Q3: How does PECS apply to bounded parameters?
Use extends
when consuming data, super
when producing data.
Q4: Can I use multiple bounds in generics?
Yes, e.g., <T extends Number & Comparable<T>>
.
Q5: How do wildcards differ from bounded type parameters?
Wildcards (?
) are used at usage sites, while bounds (extends
, super
) are applied at declaration.
Q6: Are bounded type parameters slower at runtime?
No, they compile down to type-erased bytecode.
Q7: Can I mix extends
and super
?
Yes, depending on whether the method consumes or produces data.
Q8: How does type erasure impact bounded parameters?
At runtime, bounded parameters are replaced with their bounds.
Q9: How are bounded parameters used in collections?
They allow flexible APIs, e.g., Collections.copy(List<? super T> dest, List<? extends T> src)
.
Q10: How do Spring and Hibernate use bounded parameters?
Repositories and DAOs often use extends
to constrain entity types while keeping flexibility.