Iteration is at the heart of working with collections in Java. Before Generics (Java 5), iterating over collections required manual casting from Object
, which often caused runtime errors like ClassCastException
. With the introduction of generics, Iterators became type-safe, ensuring compile-time validation of elements during iteration.
Think of a generic iterator as a conveyor belt labeled with a type: only items of the specified type travel down the belt, so you never mistakenly grab the wrong object. This eliminates casting errors and makes iteration safer, cleaner, and easier to maintain.
In this tutorial, we’ll explore how Generics and Iterators work together to provide type-safe iteration across Lists, Sets, and Maps, along with wildcards, PECS principle, and best practices.
Core Definition and Purpose of Java Generics
Generics provide:
- Type Safety – Prevent runtime
ClassCastException
. - Reusability – Write one API usable across multiple types.
- Maintainability – Simplify iteration by eliminating casting.
Introduction to Type Parameters: <T>
, <E>
, <K, V>
<T>
– General type parameter.<E>
– Element (commonly used in collections).<K, V>
– Key-Value pairs in maps.
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; }
}
Iterators Before Generics (Unsafe Iteration)
List list = new ArrayList();
list.add("Hello");
list.add(123); // Allowed
Iterator it = list.iterator();
while (it.hasNext()) {
String value = (String) it.next(); // Runtime ClassCastException!
}
Iterators with Generics (Type-Safe Iteration)
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Generics");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String value = it.next(); // No casting required
System.out.println(value);
}
Generics eliminate unsafe casting and ensure compile-time type checks.
Bounded Type Parameters: extends
and super
with Iterators
public static <T extends Number> void printNumbers(Iterator<T> iterator) {
while (iterator.hasNext()) {
System.out.println(iterator.next().doubleValue());
}
}
- Ensures only numeric types are iterated.
Wildcards in Iterators: ?
, ? extends
, ? super
?
– Unknown type.? extends T
– Upper bound (read-only).? super T
– Lower bound (write).
Example:
public static void process(Iterator<? extends Number> it) {
while (it.hasNext()) {
Number n = it.next();
System.out.println(n);
}
}
Multiple Type Parameters and Nested Generics
Iterators often appear in nested generics like:
Map<String, List<Integer>> map = new HashMap<>();
for (Map.Entry<String, List<Integer>> entry : map.entrySet()) {
Iterator<Integer> it = entry.getValue().iterator();
}
Generics in Collections Framework: Iterators in Lists, Sets, and Maps
Iterating Over Lists
List<String> list = Arrays.asList("A", "B", "C");
for (Iterator<String> it = list.iterator(); it.hasNext();) {
System.out.println(it.next());
}
Iterating Over Sets
Set<Double> set = new HashSet<>(Arrays.asList(1.1, 2.2, 3.3));
for (Iterator<Double> it = set.iterator(); it.hasNext();) {
System.out.println(it.next());
}
Iterating Over Maps
Map<String, Integer> map = Map.of("Age", 30, "Year", 2025);
for (Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator(); it.hasNext();) {
Map.Entry<String, Integer> entry = it.next();
System.out.println(entry.getKey() + " = " + entry.getValue());
}
Raw Types vs Parameterized Types in Iterators
Iterator it = list.iterator(); // Raw type - unsafe
Iterator<String> safeIt = list.iterator(); // Type-safe
Type Inference and Diamond Operator
Map<String, List<Integer>> map = new HashMap<>();
Iterator<Map.Entry<String, List<Integer>>> it = map.entrySet().iterator();
Type Erasure and Iterators
At runtime, Iterator<String>
and Iterator<Integer>
both become Iterator
due to type erasure. Compile-time checks prevent invalid usage.
Reifiable vs Non-Reifiable Types in Iterators
- Reifiable:
Iterator<?>
. - Non-Reifiable:
Iterator<String>
(erased at runtime).
Recursive Type Bounds
public static <T extends Comparable<T>> T max(Iterator<T> it) {
T max = it.next();
while (it.hasNext()) {
T current = it.next();
if (current.compareTo(max) > 0) max = current;
}
return max;
}
Designing Fluent APIs with Iterators and Generics
class FluentList<T> implements Iterable<T> {
private List<T> list = new ArrayList<>();
public FluentList<T> add(T item) { list.add(item); return this; }
public Iterator<T> iterator() { return list.iterator(); }
}
Generics with Enums and Annotations in Iterators
EnumSet<Day> days = EnumSet.of(Day.MONDAY, Day.FRIDAY);
for (Iterator<Day> it = days.iterator(); it.hasNext();) {
System.out.println(it.next());
}
Case Studies
Type-Safe Cache Iteration
class Cache<K, V> {
private Map<K, V> store = new HashMap<>();
public Iterator<Map.Entry<K, V>> iterator() {
return store.entrySet().iterator();
}
}
Flexible Repository Pattern
interface Repository<T> extends Iterable<T> {
void save(T entity);
}
Event Handling with Iterators
class EventManager<T> {
private List<T> events = new ArrayList<>();
public Iterator<T> iterator() { return events.iterator(); }
}
Best Practices for Iterators with Generics
- Always use parameterized iterators (
Iterator<String>
). - Prefer enhanced for-loops where possible (
for-each
). - Apply wildcards (
? extends
,? super
) for flexibility. - Avoid raw iterators.
Common Anti-Patterns
- Using raw iterators.
- Suppressing unchecked warnings unnecessarily.
- Designing APIs with overly complex wildcard bounds.
Performance Considerations
Generics in iterators have no runtime overhead. All checks occur at compile time due to type erasure, ensuring backward compatibility.
📌 What's New in Java for Generics?
- Java 5: Generics introduced, Iterators became type-safe.
- Java 7: Diamond operator simplified instantiations.
- Java 8: Streams and lambdas reduced manual iterator use.
- Java 10:
var
improved inference with iterators. - Java 17+: Sealed classes interact with generic collections.
- Java 21: Virtual threads enhance concurrent iteration patterns.
Conclusion and Key Takeaways
Iterators combined with generics provide type-safe iteration across collections like Lists, Sets, and Maps. They eliminate casting, prevent ClassCastException
, and improve readability. By applying wildcards, bounds, and PECS principle, developers can design robust iteration APIs that are reusable and safe.
FAQ on Iterators and Generics
Q1: Why were iterators unsafe before generics?
Because they returned Object
, requiring manual casting.
Q2: How do generics make iterators type-safe?
By binding the iterator to a specific element type.
Q3: Can I use wildcards with iterators?
Yes, Iterator<? extends T>
and Iterator<? super T>
allow flexible iteration.
Q4: Do iterators and generics affect performance?
No, checks happen at compile time.
Q5: Why can’t I check instanceof Iterator<String>
?
Type erasure removes parameterized type info at runtime.
Q6: How does the PECS principle apply to iterators?
Use extends
for reading elements, super
for writing.
Q7: What’s the difference between raw and generic iterators?
Raw iterators return Object
, while generic iterators return a type.
Q8: How are iterators used in Maps with generics?
Via Iterator<Map.Entry<K, V>>
on entrySet()
.
Q9: How do streams affect iterators?
Streams often replace explicit iterators with functional iteration.
Q10: Should I prefer iterators or for-each loops?
For-each loops for simplicity; iterators for advanced control (e.g., removal).