The Collections Framework is one of the most widely used features in Java. Before Generics (Java 5), collections stored elements as Object
, requiring explicit casting during retrieval. This often led to runtime errors such as ClassCastException
. With generics, Java introduced compile-time type safety, making collections more robust, reusable, and easier to maintain.
Think of generics in collections like labeled storage boxes. Without labels (Object
), you could put anything inside and risk pulling out the wrong item. With labels (List<String>
), you ensure only the right type goes in or out, reducing errors.
This guide explores how generics power Lists, Sets, and Maps, their use of wildcards, type bounds, and real-world best practices.
Core Definition and Purpose of Java Generics
Generics allow developers to:
- Ensure Type Safety – Catch invalid types at compile time.
- Promote Reusability – Write one collection API usable for all types.
- Simplify Code – Eliminate unnecessary casts.
Introduction to Type Parameters: <T>
, <E>
, <K, V>
<T>
: General type (Type).<E>
: Element (used inList<E>
,Set<E>
).<K, V>
: Key-Value pair (used inMap<K, V>
).
Example:
List<String> names = new ArrayList<>();
names.add("Alice");
String first = names.get(0); // No casting needed
Generic Classes with Examples
class Box<T> {
private T content;
public void set(T content) { this.content = content; }
public T get() { return content; }
}
Generic Methods: Declaration and Usage
public static <T> void printList(List<T> list) {
for (T item : list) {
System.out.println(item);
}
}
Bounded Type Parameters: extends
and super
public static <T extends Number> double sum(List<T> numbers) {
double result = 0;
for (T num : numbers) result += num.doubleValue();
return result;
}
Wildcards Explained: ?
, ? extends
, ? super
?
– Unknown type.? extends T
– Upper bound (producer).? super T
– Lower bound (consumer).
PECS Principle: Producer Extends, Consumer Super.
public static void printNumbers(List<? extends Number> list) {
for (Number n : list) System.out.println(n);
}
public static void addIntegers(List<? super Integer> list) {
list.add(42);
}
Generics in Collections Framework
1. Lists
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Generics");
for (String s : list) System.out.println(s);
- Ordered, allow duplicates.
ArrayList
,LinkedList
.
2. Sets
Set<Integer> set = new HashSet<>();
set.add(10);
set.add(20);
set.add(10); // Ignored (duplicate)
- No duplicates, no order guarantee.
HashSet
,TreeSet
,LinkedHashSet
.
3. Maps
Map<String, Integer> map = new HashMap<>();
map.put("Age", 30);
map.put("Year", 2025);
System.out.println(map.get("Age"));
- Key-Value storage.
HashMap
,TreeMap
,LinkedHashMap
.
Raw Types vs Parameterized Types
List list = new ArrayList(); // Raw type (unsafe)
list.add(100);
list.add("Java"); // Runtime risk
List<String> safeList = new ArrayList<>(); // Type-safe
Type Inference and Diamond Operator
Map<String, Integer> map = new HashMap<>(); // Diamond operator infers types
Type Erasure in Java
Generics exist at compile time only. At runtime, type information is erased:
- Compiler enforces type checks.
- JVM runs raw types.
This explains why you cannot new T()
or instanceof List<String>
.
Reifiable vs Non-Reifiable Types
- Reifiable Types: Known at runtime (
List<?>
). - Non-Reifiable Types: Lost at runtime (
List<String>
).
Recursive Type Bounds
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
Designing 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
EnumSet<Day> days = EnumSet.of(Day.MONDAY, Day.FRIDAY);
@SuppressWarnings("unchecked")
Generics with Exceptions
- Cannot
catch (T e)
. - Cannot
throw new T()
.
Generics and Reflection
Reflection extracts generic info with ParameterizedType
:
Field field = MyClass.class.getDeclaredField("list");
Type type = field.getGenericType();
Case Studies
Type-Safe Cache
class Cache<K, V> {
private Map<K, V> store = new HashMap<>();
public void put(K key, V value) { store.put(key, value); }
public V get(K key) { return store.get(key); }
}
Flexible Repository Pattern
interface Repository<T, ID> {
void save(T entity);
T findById(ID id);
}
Event Handling
interface EventListener<T> {
void onEvent(T event);
}
Best Practices for Generics in Collections
- Prefer generics over raw types.
- Use wildcards for API flexibility.
- Apply PECS properly.
- Favor
List
overArrayList
in variable declarations (use interfaces). - Keep type signatures clean.
Common Anti-Patterns
- Suppressing warnings unnecessarily.
- Using raw types in modern code.
- Overly complex nested generics.
Performance Considerations
Generics do not slow performance — they are enforced at compile time and erased at runtime. Collections remain efficient while safer to use.
📌 What's New in Java for Generics?
- Java 5: Generics introduced in Collections.
- Java 7: Diamond operator (
<>
). - Java 8: Streams use generics heavily (
map
,filter
). - Java 10:
var
works with generics. - Java 17+: Sealed classes work with generic hierarchies.
- Java 21: Virtual threads enable scalable concurrency with generics.
Conclusion and Key Takeaways
Generics revolutionized the Collections Framework by making List
, Set
, and Map
type-safe and reusable. They prevent runtime ClassCastException
, simplify APIs, and support advanced use cases with bounds, wildcards, and type inference. By mastering generics with collections, developers can write cleaner, safer, and more flexible code.
FAQ on Generics with Collections
Q1: Why can’t I use raw types in collections?
Because they bypass compile-time safety, leading to runtime errors.
Q2: What’s the difference between List<Object>
and List<?>
?List<Object>
accepts only Object
or subtypes, List<?>
accepts any type.
Q3: Can I store primitives in collections?
No, use wrapper types (Integer
, Double
).
Q4: Why can’t I create arrays of generics?
Because arrays are reifiable, but generics are erased.
Q5: How does the PECS principle apply to collections?
Use ? extends
for producers, ? super
for consumers.
Q6: How do generics prevent ClassCastException
?
By enforcing compile-time type checks.
Q7: Can I use wildcards in Maps?
Yes, e.g., Map<? extends Number, ? super String>
.
Q8: Are generics slower in collections?
No, erasure ensures no runtime cost.
Q9: How are generics used in Streams API?
Stream methods (map
, filter
) rely on generics for type safety.
Q10: What’s the best practice for declaring collections?
Use interfaces (List
, Set
, Map
) in variable declarations, not implementations.