CMP-220 · Lesson 9 of 12 · 21 min
Generics, templates and collections
In CMP-200 Jonah built stacks, queues, hash tables and heaps by hand, each one for a particular kind of element: detections in one, timers in another. Object-oriented languages let you write a data structure once and use it for any element type, safely, through generics in Java, templates in C++ and type parameters in Python's type hints. They also ship large libraries of ready-made, well-tested collections built this way. This lesson explains how generic types work, the surprising rules that govern them in Java, how C++ templates differ, and how to choose among the Java collections, connecting each to the data structure behind it.
The problem generics solve
Before Java 5, the standard collections stored elements as Object, the root of every class hierarchy. A list could hold anything, so every read needed a cast: Detection d = (Detection) list.get(i). If someone had put a String into the list by mistake, the cast failed at run time with a ClassCastException, possibly far from where the wrong object was added. The compiler could not help, because to it the list was just a list of Objects. Tally.java still contains raw lists of this kind, and one of them once mixed Integer node identifiers with Long ones, causing lookups to fail silently because Integer.valueOf(17) does not equal Long.valueOf(17).
Generic classes and methods
A generic class declares one or more type parameters in angle brackets, conventionally single capital letters such as T for type, E for element, K and V for key and value. Code that uses the class supplies type arguments: List<Detection> is a list whose elements are Detection objects, and the compiler checks every add and inserts every cast automatically. Errors that used to appear at run time, if at all, now stop compilation at the line that is wrong. A generic method declares its own type parameters before its return type, so one method can work on many types while keeping their relationships, such as returning the same type it received.
/** A fixed-capacity buffer that keeps the most recent items, for any element type. */
public final class RecentBuffer<T> implements Iterable<T> {
private final Object[] items; // generic arrays cannot be created directly (erasure)
private int next = 0, size = 0;
public RecentBuffer(int capacity) { items = new Object[capacity]; }
public void add(T item) {
items[next] = item;
next = (next + 1) % items.length;
size = Math.min(size + 1, items.length);
}
public T newest() { // unchecked cast: safe, add() stores only T
if (size == 0) throw new IllegalStateException("empty");
return (T) items[(next - 1 + items.length) % items.length];
}
@Override public Iterator<T> iterator() { /* oldest to newest */ ... }
}
RecentBuffer<Alert> recentAlerts = new RecentBuffer<>(50); // diamond: type inferred
recentAlerts.add(new Alert(4417, "silent 30 min"));
Alert a = recentAlerts.newest(); // no cast needed
// recentAlerts.add("text"); // compile error: String is not an Alert
public static <T extends Comparable<T>> T maxOf(List<T> xs) { // a bounded generic method
T best = xs.get(0);
for (T x : xs) if (x.compareTo(best) > 0) best = x;
return best;
}The method maxOf shows a bounded type parameter. Writing <T extends Comparable<T>> says that T may be any type whose objects can be compared with each other, so the method body may call compareTo. Without the bound, the compiler would know nothing about T and would reject the call. Bounds let generic code use the operations it needs while still accepting every suitable type.
Type erasure and its consequences
Java implements generics by type erasure: the compiler checks the type arguments, inserts the necessary casts, and then erases the type parameters, so at run time a List<Detection> and a List<String> are both just List. Erasure kept new generic code compatible with older libraries, but it has consequences you will meet. You cannot write new T() or new T[10], because the type is not known at run time, which is why RecentBuffer stores an Object array and casts. You cannot test x instanceof List<Detection>. Primitive types cannot be type arguments, so you write List<Integer>, and each int is boxed into an Integer object, costing memory and time; for large numeric arrays, plain int[] remains far more efficient. The cast to T in newest() is unchecked: the compiler cannot verify it and flags it at compile time, so the programmer must ensure, as add() does here, that only T values are ever stored, and can then silence the message with an annotation on the method.
Wildcards and variance
A puzzle confuses almost everyone at first. A LoopNode is a CounterNode, so is a List<LoopNode> a List<CounterNode>? In Java, no. If it were, code could take the List<LoopNode>, view it as a List<CounterNode>, and add a RadarNode to it, corrupting a list that is supposed to hold only loop nodes. Generic types are therefore invariant. When a method only reads from a collection, declare the parameter as List<? extends CounterNode>, which accepts a list of CounterNode or of any subclass, but forbids adding. When a method only writes into a collection, declare it as List<? super LoopNode>, which accepts lists of LoopNode or any supertype. Joshua Bloch summarized the rule as PECS: producer extends, consumer super.
// Reads only: accepts List<CounterNode>, List<LoopNode>, List<RadarNode>...
static long countSilent(List<? extends CounterNode> nodes, Instant now) {
return nodes.stream().filter(n -> n.isSilent(now, Duration.ofMinutes(30))).count();
}
// Writes only: accepts List<LoopNode>, List<CounterNode>, List<Object>
static void loadLoopNodes(List<? super LoopNode> out, Config cfg) {
for (LoopConfig lc : cfg.loops()) out.add(new LoopNode(lc.id(), lc.lanes(), lc.spacingCm()));
}C++ templates
C++ templates look similar but work differently. A template is a pattern from which the compiler generates a separate, fully typed copy of the code for each set of template arguments it is used with, a process called instantiation. std::vector<Detection> and std::vector<int> become two different classes in the compiled program. Nothing is erased, so templates work with primitive types without boxing, can create objects of type T, and are fully optimized for each type, which is why C++ containers are as fast as hand-written code. The costs are longer compile times, larger executables when many instantiations are generated, and, historically, notoriously long error messages. C++20 concepts let a template state its requirements, such as std::totally_ordered<T>, much as Java's bounds do, which makes error messages far clearer. Templates are also Turing-complete at compile time, which enables template metaprogramming, a powerful technique best used sparingly.
// C++20: one template, separate optimized code for each element type
template <std::totally_ordered T>
const T& max_of(const std::vector<T>& xs) {
const T* best = &xs.front();
for (const T& x : xs) if (*best < x) best = &x;
return *best;
}
std::vector<int> speeds{62, 88, 54};
std::vector<std::string> names{"Princess", "Bath", "Division"};
int top = max_of(speeds); // instantiates max_of<int>
const std::string& last = max_of(names); // instantiates max_of<std::string>The Java collections framework
Java's standard library organizes its collections around a small set of interfaces, each with several implementations whose performance follows directly from the data structures in CMP-200. Collection is the root interface for groups of elements. List keeps elements in order with positional access. Set holds no duplicates. Queue and Deque support adding and removing at the ends. Map, which is not a Collection, associates keys with values. Following lesson 7, declare variables with these interface types and choose the implementation once, when you construct the object.
| Need | Interface | Implementation | Structure | Key costs |
|---|---|---|---|---|
| Ordered list, index access | List | ArrayList | Dynamic array | get O(1); add at end O(1) amortized; insert in middle O(n) |
| Fast membership test | Set | HashSet | Hash table | add, contains O(1) average |
| Sorted set, range queries | SortedSet | TreeSet | Red-black tree | add, contains O(log n); ordered iteration |
| Look up by key | Map | HashMap | Hash table | get, put O(1) average |
| Keys in sorted order | SortedMap | TreeMap | Red-black tree | get, put O(log n); floorKey, subMap |
| Enum keys | Map | EnumMap | Array indexed by ordinal | O(1), very compact |
| Stack or queue | Deque | ArrayDeque | Circular array | add, remove at ends O(1) amortized |
| Next by priority | Queue | PriorityQueue | Binary heap | offer, poll O(log n); peek O(1) |
Two families of interfaces control ordering. A class that has one natural order implements Comparable<T> and its compareTo method, as String, Integer and Instant do. When you need a different order, or the class has none, pass a Comparator<T>, a separate object, often a lambda, that compares two elements: Comparator.comparing(Detection::time).thenComparing(Detection::lane). TreeMap, TreeSet, PriorityQueue and the sort methods all accept a comparator. For TreeSet and TreeMap the comparator must be consistent with equals, or elements that compare as equal will silently replace each other.
Collections are traversed with iterators, objects that implement Iterator<T> with hasNext and next, and the for-each loop uses them behind the scenes for anything Iterable. Modifying most collections while iterating over them with for-each makes the iterator throw ConcurrentModificationException at the next step, a deliberate fail-fast check; remove elements with the iterator's own remove method or with removeIf. Finally, List.of, Set.of and Map.of create unmodifiable collections, ideal for constants and for the defensive copies of lesson 2.
Worked problem
Choosing collections for the interval counter
Every 15 minutes the Hub must produce, for each station, a count of vehicles per lane and per class, and then a report listing intervals in time order and supporting queries such as all intervals between 07:00 and 09:00. There are five vehicle classes, up to eight lanes and 96 intervals per day. Choose collection types.
Show solution
- Counts per class: the keys are a fixed enum, VehicleClass, so EnumMap<VehicleClass, Integer> gives array speed and compactness.
- Counts per lane: lanes are small integers 1 to 8, so a List of eight such maps, indexed by lane − 1, is simplest.
- Intervals by time with range queries: the keys must stay sorted, so a TreeMap<Instant, IntervalCount> lets subMap(start, end) return the morning peak in O(log n + k).
- Declare the fields as Map, List and NavigableMap, the interface types, and construct the specific classes in the constructor.
Answer: EnumMap inside a List for counts, and a TreeMap keyed by interval start for the time series. Each choice follows from the operations needed, exactly as in CMP-200.
Generics in Python
Python's built-in list, dict and set hold any objects, and type annotations such as list[Detection] or dict[int, Node] have no effect at run time; they exist for readers and for static checkers such as mypy. Generic classes of your own use TypeVar or, since Python 3.12, the syntax class RecentBuffer[T]. Python's standard library provides the same structures as Java's: collections.deque, heapq for binary heaps, collections.Counter for counting and sorted with a key function in place of a Comparator.
from collections import Counter, deque
from typing import Generic, TypeVar, Iterator
import heapq
T = TypeVar("T")
class RecentBuffer(Generic[T]):
def __init__(self, capacity: int):
self._items: deque[T] = deque(maxlen=capacity) # a ring buffer from the library
def add(self, item: T) -> None:
self._items.append(item)
def newest(self) -> T:
return self._items[-1]
def __iter__(self) -> Iterator[T]:
return iter(self._items)
alerts: RecentBuffer[str] = RecentBuffer(3)
for a in ["n4417 silent", "n311 low battery", "n17 lane 9", "n902 glare"]:
alerts.add(a)
print("recent:", list(alerts), "| newest:", alerts.newest())
dets = [("07:42:13", 2, "car"), ("07:42:09", 1, "bus"), ("07:42:13", 1, "car"),
("07:41:58", 2, "heavy truck"), ("07:42:01", 1, "car")]
by_time_then_lane = sorted(dets, key=lambda d: (d[0], d[1])) # like a Comparator chain
print("first:", by_time_then_lane[0], "| last:", by_time_then_lane[-1])
print("per class:", dict(Counter(cls for _, _, cls in dets)))
print("two longest gaps (s):", heapq.nlargest(2, [4, 11, 3, 15, 8]))
lanes = [1, 2, 9, 3, 9, 4]
try:
for x in lanes: # modifying while iterating is a classic bug
if x == 9:
lanes.remove(x)
except RuntimeError:
pass
print("after naive removal:", lanes, "(one 9 survived)")
lanes = [x for x in [1, 2, 9, 3, 9, 4] if x != 9]
print("after filtering into a new list:", lanes)recent: ['n311 low battery', 'n17 lane 9', 'n902 glare'] | newest: n902 glare
first: ('07:41:58', 2, 'heavy truck') | last: ('07:42:13', 2, 'car')
per class: {'car': 3, 'bus': 1, 'heavy truck': 1}
two longest gaps (s): [15, 11]
after naive removal: [1, 2, 3, 4] (one 9 survived)
after filtering into a new list: [1, 2, 3, 4]The last part of the example shows why Java's fail-fast iterators exist. Python lists do not detect modification during iteration, so removing an element shifts the rest left and the loop skips the element that moved into the removed one's place; one 9 survives without any error. Building a new list, or iterating over a copy, avoids the problem in both languages.
Common mistakes
MistakeUsing raw types such as List without a type argument.
InsteadAlways supply type arguments so the compiler checks every element; raw types exist only for old code.
MistakeExpecting List<LoopNode> to be accepted where List<CounterNode> is required.
InsteadGeneric types are invariant; use List<? extends CounterNode> for parameters that only read.
MistakeStoring millions of numbers in List<Integer>.
InsteadBoxing costs memory and time; use int[] or a primitive-specialized structure for large numeric data.
MistakeRemoving elements inside a for-each loop.
InsteadUse Iterator.remove, removeIf, or build a new collection.
MistakeChoosing a collection without thinking about its operations.
InsteadPick the implementation from the operations and their costs, as in CMP-200, and declare the variable with the interface type.
Summary and key terms
- Generics let one class or method work with many element types while the compiler checks types and inserts casts.
- Bounded type parameters (T extends Comparable<T>) let generic code call the operations it needs.
- Java erases type arguments at run time: no new T(), no primitive type arguments, and generic types are invariant; use ? extends to read and ? super to write (PECS).
- C++ templates generate specialized code for each type at compile time; concepts state requirements.
- The Java collections framework pairs interfaces (List, Set, Map, Deque, Queue) with implementations whose costs follow from their data structures.
- Generic type
- A class or interface with type parameters, such as List<E>.
- Type parameter
- A placeholder for a type, written in angle brackets and supplied when the generic type is used.
- Bounded type parameter
- A type parameter restricted to subtypes of a given type, such as T extends Comparable<T>.
- Type erasure
- Java's removal of type arguments after compile-time checking.
- Wildcard
- A ? in a generic type, optionally bounded with extends or super, that accepts a family of type arguments.
- Template instantiation
- In C++, the compiler generating concrete code from a template for specific type arguments.
Practice problems
Work each problem on paper first, then reveal the answer to check.
Why does the compiler reject adding a String to a RecentBuffer<Alert>?
Show answer
The type argument is Alert, and a String is not an Alert.
Which collection would you use to find, in sorted order, all detections between two times?
Show answer
A TreeMap (or TreeSet) keyed by time, using subMap or subSet.
What does PECS stand for, and which wildcard does a method that only adds elements need?
Show answer
Producer extends, consumer super; a method that only adds needs ? super T.
Name two consequences of type erasure in Java.
Show answer
You cannot create new T() or new T[n], and you cannot use primitive types as type arguments (or test instanceof List<String>).
What is the main runtime advantage of C++ templates over Java generics?
Show answer
Specialized, fully optimized code for each type, with no boxing or casts.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.