Java Generics in the Collections Framework
Generics in the Collections Framework
The Java Collections Framework and generics arrived together in Java 5, and that was not a coincidence — every core collection type was redesigned around type parameters at the same time. Before Java 5, List, Map, and Set stored raw Object references. Every retrieval needed a cast. Every cast was a runtime gamble. Generics changed the contract: List<E>, Map<K,V>, and Set<E> now carry the element type as part of their signature, and the compiler verifies every operation against it. This article brings together every concept from the Generics series — type parameters, bounded types, wildcards, PECS, and iterators — and shows where each one appears in the collections you write every day.
What Generics Changed for Collections
The practical difference is where errors appear. Without generics, a wrong-type insertion fails at the cast site — at runtime, possibly in production. With generics, it fails at the insertion site — at compile time, during development.
PRE-GENERICS (Java 1.4):
List orders = new ArrayList();
orders.add("ORD-001");
orders.add(42); // silently accepted - no error
String id = (String) orders.get(0); // cast - works here
String bad = (String) orders.get(1); // ClassCastException at RUNTIME
Map inventory = new HashMap();
inventory.put("P001", 120);
int qty = (int) inventory.get("P001"); // cast required - no guarantee
POST-GENERICS (Java 5+):
List<String> orders = new ArrayList<>();
orders.add("ORD-001");
// orders.add(42); <- COMPILE ERROR - caught immediately
String id = orders.get(0); // no cast - compiler knows it is String
Map<String, Integer> inventory = new HashMap<>();
inventory.put("P001", 120);
int qty = inventory.get("P001"); // Integer auto-unboxed - no cast
THE STRUCTURAL SHIFT:
Type mismatches moved from RUNTIME ClassCastException
to COMPILE-TIME error.
The cast is still in the bytecode - but the compiler verified it is correct.
Basic Overview - How Generics Appear Across the Collections API
1. TYPE PARAMETERS ON EVERY COLLECTION INTERFACE
Fresher view : every collection declares a type parameter.
List<E>, Set<E>, Queue<E>, Map<K,V>, Iterator<E>.
When you write List<String>, E = String everywhere:
add(String), get() returns String, no casts anywhere.
Deeper view : the compiler tracks E at every usage site.
List<String>.get() compiles to Object in bytecode
(type erasure) with an automatic CHECKCAST instruction
that the compiler proves will never fail.
Two parameterizations List<String> and List<Integer>
share the same ArrayList.class at runtime.
2. WILDCARDS IN COLLECTION UTILITY METHODS
Fresher view : Collections.sort(), Collections.copy(), addAll()
accept wildcards so one method works with
List<Integer>, List<Double>, List<String> - not
just one exact parameterized type.
Deeper view : Collections.copy(List<? super T> dest,
List<? extends T> src) uses full PECS.
src is the producer (? extends - read from it).
dest is the consumer (? super - write into it).
This lets dest be List<Number> when src is
List<Integer> - Integer IS-A Number.
3. BOUNDED TYPES IN SORTED COLLECTIONS
Fresher view : TreeSet and TreeMap keep elements in order.
Ordering requires comparison - so elements must
implement Comparable, or a Comparator must be
provided. The generic API enforces this choice.
Deeper view : TreeSet<E>'s no-arg constructor requires E to
implement Comparable<? super E> at runtime -
violation throws ClassCastException on first insert.
The Comparator overload TreeSet(Comparator<? super E>)
accepts any comparator for E or its supertypes,
applying the PECS consumer-super pattern.
4. Iterator<E> AND THE FOR-EACH LOOP
Fresher view : the for-each loop works because collections
implement Iterable<E>, whose iterator() method
returns Iterator<E>. That Iterator's next()
returns E - the exact element type, no cast.
Deeper view : the compiler translates for (String s : list)
into a while loop using list.iterator() and
it.next(). The CHECKCAST the compiler inserts
for the E return type is verified correct at
compile time. No cast is ever written in
application code, but the type safety guarantee
holds end to end.
List - Type-Safe Element Sequences
List<E> is the foundation of most collection usage. Its type parameter E propagates through every method: add(E), get(int) returning E, set(int, E), indexOf(Object), contains(Object), iterator() returning Iterator<E>. The two Object-based methods (contains, indexOf) remain Object for backward compatibility — they always return false or -1 rather than throwing when passed the wrong type.
// File: ListGenericsDemo.java
import java.util.*;
import java.util.function.*;
public class ListGenericsDemo {
// Generic method with bounded param - sorts any Comparable list
static <T extends Comparable<T>> List<T> sortedCopy(List<T> source) {
List<T> copy = new ArrayList<>(source);
Collections.sort(copy);
return copy;
}
// PECS in a custom utility - reads from producer, writes to consumer
static <T> void copyIf(List<? extends T> source,
List<? super T> dest,
Predicate<? super T> predicate) {
for (T item : source) { // source PRODUCES T (? extends)
if (predicate.test(item)) {
dest.add(item); // dest CONSUMES T (? super)
}
}
}
public static void main(String[] args) {
List<String> products = new ArrayList<>(
List.of("USB Hub", "Wireless Mouse", "Keyboard", "Laptop Stand", "Webcam"));
System.out.println("=== sortedCopy - bounded <T extends Comparable<T>> ===");
List<String> sorted = sortedCopy(products);
System.out.println("Original : " + products);
System.out.println("Sorted : " + sorted);
System.out.println();
System.out.println("=== sortedCopy - same method with List<Integer> ===");
List<Integer> prices = new ArrayList<>(List.of(1299, 499, 2499, 799, 3499));
List<Integer> sortedPrices = sortedCopy(prices);
System.out.println("Sorted prices: " + sortedPrices);
System.out.println();
System.out.println("=== copyIf - PECS: ? extends source, ? super dest ===");
List<Object> destObj = new ArrayList<>(); // List<Object> satisfies ? super String
copyIf(products, destObj, name -> name.length() > 7);
System.out.println("Names longer than 7 chars (into List<Object>): " + destObj);
System.out.println();
System.out.println("=== for-each uses Iterator<E> - no cast in sight ===");
int total = 0;
for (int price : prices) { // Integer unboxed to int - no explicit cast
total += price;
}
System.out.println("Total: Rs." + total);
System.out.println();
System.out.println("=== Collections.addAll uses ? super T internally ===");
List<Number> numList = new ArrayList<>();
Collections.addAll(numList, 10, 20, 30); // Integer added - Number satisfies ? super Integer
System.out.println("numList: " + numList);
}
}Output:
=== sortedCopy - bounded <T extends Comparable<T>> ===
Original : [USB Hub, Wireless Mouse, Keyboard, Laptop Stand, Webcam]
Sorted : [Keyboard, Laptop Stand, USB Hub, Webcam, Wireless Mouse]
=== sortedCopy - same method with List<Integer> ===
Sorted prices: [499, 799, 1299, 2499, 3499]
=== copyIf - PECS: ? extends source, ? super dest ===
Names longer than 7 chars (into List<Object>): [Wireless Mouse, Keyboard, Laptop Stand, Webcam]
=== for-each uses Iterator<E> - no cast in sight ===
Total: Rs.8495
=== Collections.addAll uses ? super T internally ===
numList: [10, 20, 30]
Map<K,V> - Two Independent Type Parameters
Map<K,V> is the canonical two-type-parameter design. K and V are completely independent placeholders — a Map<String, Integer> uses String for keys and Integer for values, and neither constrains the other. The iteration entry type Map.Entry<K,V> inherits both parameters, giving both getKey() returning K and getValue() returning V fully typed at the call site.
// File: MapGenericsDemo.java
import java.util.*;
import java.util.function.*;
public class MapGenericsDemo {
// Inverts Map<K,V> to Map<V,K> - uses Map.Entry<K,V> to iterate
static <K, V> Map<V, K> invert(Map<K, V> source) {
Map<V, K> result = new LinkedHashMap<>();
for (Map.Entry<K, V> entry : source.entrySet()) {
K key = entry.getKey(); // K - typed, no cast
V value = entry.getValue(); // V - typed, no cast
result.put(value, key);
}
return result;
}
// Groups items by a key function - List<T> -> Map<K, List<T>>
static <T, K> Map<K, List<T>> groupBy(List<T> items, Function<T, K> keyFn) {
Map<K, List<T>> groups = new LinkedHashMap<>();
for (T item : items) {
groups.computeIfAbsent(keyFn.apply(item), k -> new ArrayList<>()).add(item);
}
return groups;
}
// Merges two maps using ? extends K and ? extends V - accepts any subtype maps
static <K, V> Map<K, V> merge(Map<? extends K, ? extends V> first,
Map<? extends K, ? extends V> second) {
Map<K, V> result = new LinkedHashMap<>(first);
result.putAll(second); // putAll accepts Map<? extends K, ? extends V>
return result;
}
public static void main(String[] args) {
Map<String, Integer> stockLevels = new LinkedHashMap<>();
stockLevels.put("P001-Mouse", 120);
stockLevels.put("P002-Stand", 45);
stockLevels.put("P003-USBHub", 200);
stockLevels.put("P004-Keyboard", 28);
System.out.println("=== Map.Entry<K,V> - both key and value fully typed ===");
for (Map.Entry<String, Integer> entry : stockLevels.entrySet()) {
String id = entry.getKey(); // String - no cast
Integer qty = entry.getValue(); // Integer - no cast
String status = qty < 50 ? "LOW" : "OK";
System.out.printf(" %-22s qty=%-4d [%s]%n", id, qty, status);
}
System.out.println();
System.out.println("=== invert - Map<String,Integer> to Map<Integer,String> ===");
Map<Integer, String> inverted = invert(stockLevels);
inverted.forEach((qty, id) -> System.out.printf(" qty=%-4d -> %s%n", qty, id));
System.out.println();
System.out.println("=== groupBy - group product IDs by stock status ===");
List<String> ids = List.of("P001-Mouse", "P002-Stand", "P003-USBHub", "P004-Keyboard");
Map<String, List<String>> byStatus = groupBy(
ids, id -> stockLevels.getOrDefault(id, 0) >= 50 ? "IN_STOCK" : "LOW_STOCK");
byStatus.forEach((status, list) ->
System.out.println(" " + status + ": " + list));
System.out.println();
System.out.println("=== merge two maps with ? extends K, ? extends V ===");
Map<String, Integer> warehouseA = Map.of("P001-Mouse", 60, "P003-USBHub", 100);
Map<String, Integer> warehouseB = Map.of("P002-Stand", 30, "P004-Keyboard", 15);
Map<String, Integer> combined = merge(warehouseA, warehouseB);
System.out.println(" Combined: " + combined);
}
}Output:
=== Map.Entry<K,V> - both key and value fully typed ===
P001-Mouse qty=120 [OK]
P002-Stand qty=45 [LOW]
P003-USBHub qty=200 [OK]
P004-Keyboard qty=28 [LOW]
=== invert - Map<String,Integer> to Map<Integer,String> ===
qty=120 -> P001-Mouse
qty=45 -> P002-Stand
qty=200 -> P003-USBHub
qty=28 -> P004-Keyboard
=== groupBy - group product IDs by stock status ===
IN_STOCK: [P001-Mouse, P003-USBHub]
LOW_STOCK: [P002-Stand, P004-Keyboard]
=== merge two maps with ? extends K, ? extends V ===
Combined: {P001-Mouse=60, P003-USBHub=100, P002-Stand=30, P004-Keyboard=15}
Set and Sorted Collections
Set<E> uses one type parameter, but sorted sets — TreeSet<E> and SortedSet<E> — surface a concrete generic contract: either E must implement Comparable<? super E>, or a Comparator<? super E> must be supplied. The wildcard on the comparator is the PECS consumer pattern: the comparator consumes E values, so ? super E allows a Comparator<Object> to sort a Set<Product>.
// File: SetGenericsDemo.java
import java.util.*;
public class SetGenericsDemo {
record Product(String id, String name, double price) {}
public static void main(String[] args) {
System.out.println("=== HashSet<String> - type-safe, no duplicates ===");
Set<String> categories = new HashSet<>();
categories.add("Electronics");
categories.add("Accessories");
categories.add("Electronics"); // silently deduplicated
categories.add("Furniture");
System.out.println("Categories : " + categories);
System.out.println("Has Electronics: " + categories.contains("Electronics"));
System.out.println();
System.out.println("=== TreeSet<String> - natural order via Comparable<String> ===");
TreeSet<String> sorted = new TreeSet<>(categories);
System.out.println("Sorted : " + sorted);
System.out.println("First : " + sorted.first());
System.out.println("Last : " + sorted.last());
System.out.println("headSet(<F) : " + sorted.headSet("F"));
System.out.println();
System.out.println("=== TreeSet with Comparator<? super Product> ===");
// Product does NOT implement Comparable - Comparator is required
// Comparator<Product> satisfies Comparator<? super Product>
Comparator<Product> byPrice = Comparator.comparingDouble(Product::price);
TreeSet<Product> byPriceSet = new TreeSet<>(byPrice);
byPriceSet.add(new Product("P001", "Wireless Mouse", 799.0));
byPriceSet.add(new Product("P002", "Laptop Stand", 1299.0));
byPriceSet.add(new Product("P003", "USB Hub", 499.0));
byPriceSet.add(new Product("P004", "Keyboard", 2499.0));
System.out.println("Products sorted by price:");
byPriceSet.forEach(p ->
System.out.printf(" %-20s Rs.%.2f%n", p.name(), p.price()));
System.out.println();
System.out.println("=== Set operations - addAll / retainAll use ? extends E / ? ===");
Set<Integer> setA = new HashSet<>(Set.of(499, 799, 1299, 2499));
Set<Integer> setB = new HashSet<>(Set.of(799, 1299, 3499));
Set<Integer> union = new HashSet<>(setA);
union.addAll(setB); // addAll accepts Collection<? extends Integer>
System.out.println("Union : " + new TreeSet<>(union));
Set<Integer> intersection = new HashSet<>(setA);
intersection.retainAll(setB); // retainAll accepts Collection<?>
System.out.println("Intersection: " + new TreeSet<>(intersection));
}
}Output:
=== HashSet<String> - type-safe, no duplicates ===
Categories : [Accessories, Electronics, Furniture]
Has Electronics: true
=== TreeSet<String> - natural order via Comparable<String> ===
Sorted : [Accessories, Electronics, Furniture]
First : Accessories
Last : Furniture
headSet(<F) : [Accessories, Electronics]
=== TreeSet with Comparator<? super Product> ===
Products sorted by price:
USB Hub Rs.499.00
Wireless Mouse Rs.799.00
Laptop Stand Rs.1299.00
Keyboard Rs.2499.00
=== Set operations - addAll / retainAll use ? extends E / ? ===
Union : [499, 799, 1299, 2499, 3499]
Intersection: [799, 1299]
Collections Utility Methods and PECS
java.util.Collections is a non-generic class containing exclusively generic static methods. Its signatures are the most instructive PECS examples in the JDK. Reading each signature tells you exactly how the method uses its parameters.
// File: CollectionsUtilityDemo.java
import java.util.*;
public class CollectionsUtilityDemo {
public static void main(String[] args) {
System.out.println("=== Collections.sort with Comparator<? super T> ===");
// sort(List<T> list, Comparator<? super T> c)
// Comparator<Object> satisfies Comparator<? super String>
List<String> items = new ArrayList<>(
List.of("Webcam", "Keyboard", "Hub", "Mouse", "Monitor"));
Collections.sort(items); // natural order
System.out.println("Natural : " + items);
Collections.sort(items, Comparator.comparingInt(String::length));
System.out.println("By length: " + items);
System.out.println();
System.out.println("=== Collections.copy - PECS in one signature ===");
// copy(List<? super T> dest, List<? extends T> src)
// src PRODUCES T values -> ? extends T (producer)
// dest CONSUMES T values -> ? super T (consumer)
List<Integer> source = List.of(499, 799, 1299, 2499);
List<Number> dest = new ArrayList<>(Arrays.asList(0, 0, 0, 0));
Collections.copy(dest, source);
// List<Number> satisfies ? super Integer (Number IS a supertype of Integer)
System.out.println("Copied into List<Number>: " + dest);
System.out.println();
System.out.println("=== Collections.min / max - Comparable or Comparator<? super T> ===");
List<Integer> scores = List.of(72, 95, 88, 64, 91, 79);
System.out.println("Min: " + Collections.min(scores));
System.out.println("Max: " + Collections.max(scores));
System.out.println();
System.out.println("=== Collections.frequency - Object parameter for backwards compat ===");
List<String> tags = List.of("sale", "new", "sale", "trending", "new", "sale");
System.out.println("'sale' count : " + Collections.frequency(tags, "sale"));
System.out.println("'trending' count: " + Collections.frequency(tags, "trending"));
System.out.println();
System.out.println("=== Collections.unmodifiableList - preserves type parameter ===");
List<String> mutable = new ArrayList<>(List.of("Alpha", "Beta", "Gamma"));
List<String> immutable = Collections.unmodifiableList(mutable);
String first = immutable.get(0); // String - type parameter preserved
System.out.println("get(0) as String: " + first.toUpperCase());
try {
immutable.add("Delta");
} catch (UnsupportedOperationException e) {
System.out.println("add() blocked: UnsupportedOperationException");
}
}
}Output:
=== Collections.sort with Comparator<? super T> ===
Natural : [Hub, Keyboard, Monitor, Mouse, Webcam]
By length: [Hub, Mouse, Webcam, Monitor, Keyboard]
=== Collections.copy - PECS in one signature ===
Copied into List<Number>: [499, 799, 1299, 2499]
=== Collections.min / max - Comparable or Comparator<? super T> ===
Min: 64
Max: 95
=== Collections.frequency - Object parameter for backwards compat ===
'sale' count : 3
'trending' count: 1
=== Collections.unmodifiableList - preserves type parameter ===
get(0) as String: ALPHA
add() blocked: UnsupportedOperationException
Iterator and Iterable
The for-each loop is built on two generic interfaces. Iterable<T> declares Iterator<T> iterator(). Iterator<T> declares T next(), boolean hasNext(), and void remove(). When the compiler encounters for (String s : list), it generates a while loop using list.iterator() typed as Iterator<String> — next() returns String directly, with no explicit cast anywhere in application code.
// File: IteratorGenericsDemo.java
import java.util.*;
import java.util.function.*;
public class IteratorGenericsDemo {
// Custom generic Iterable - implements Iterable<T> for any type T
static class FilteredIterable<T> implements Iterable<T> {
private final List<T> source;
private final Predicate<T> filter;
FilteredIterable(List<T> source, Predicate<T> filter) {
this.source = List.copyOf(source);
this.filter = filter;
}
@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
private int index = 0;
private T pending;
private boolean ready = false;
private boolean advance() {
while (index < source.size()) {
T candidate = source.get(index++);
if (filter.test(candidate)) {
pending = candidate;
ready = true;
return true;
}
}
ready = false;
return false;
}
@Override public boolean hasNext() { return ready || advance(); }
@Override
public T next() {
if (!ready && !advance()) throw new NoSuchElementException();
ready = false;
return pending; // returns T - fully typed
}
};
}
}
public static void main(String[] args) {
System.out.println("=== Manual Iterator<String> - next() returns String ===");
List<String> cities = List.of("Mumbai", "Pune", "Nashik", "Nagpur", "Aurangabad");
Iterator<String> it = cities.iterator();
while (it.hasNext()) {
String city = it.next(); // String - no cast
System.out.println(" " + city.toUpperCase());
}
System.out.println();
System.out.println("=== for-each compiles to the same Iterator<String> loop ===");
for (String city : cities) { // identical bytecode to above
System.out.println(" " + city.length() + " chars");
}
System.out.println();
System.out.println("=== Custom FilteredIterable<Integer> - only prices > 1000 ===");
List<Integer> allPrices = List.of(499, 1299, 799, 2499, 399, 1799, 649);
FilteredIterable<Integer> expensive =
new FilteredIterable<>(allPrices, price -> price > 1000);
System.out.print("Expensive: ");
for (Integer price : expensive) { // for-each uses our generic Iterator<Integer>
System.out.print("Rs." + price + " ");
}
System.out.println();
System.out.println();
System.out.println("=== Safe removal via Iterator.remove() ===");
List<Integer> mutablePrices = new ArrayList<>(
List.of(499, 1299, 799, 2499, 399, 1799));
Iterator<Integer> remover = mutablePrices.iterator();
while (remover.hasNext()) {
Integer price = remover.next(); // Integer - typed by Iterator<Integer>
if (price < 1000) remover.remove(); // safe in-loop removal
}
System.out.println("Remaining (>= Rs.1000): " + mutablePrices);
}
}Output:
=== Manual Iterator<String> - next() returns String ===
MUMBAI
PUNE
NASHIK
NAGPUR
AURANGABAD
=== for-each compiles to the same Iterator<String> loop ===
6 chars
4 chars
6 chars
6 chars
9 chars
=== Custom FilteredIterable<Integer> - only prices > 1000 ===
Expensive: Rs.1299 Rs.2499 Rs.1799
=== Safe removal via Iterator.remove() ===
Remaining (>= Rs.1000): [1299, 2499, 1799]
Basic Example
import java.util.*;
public class Main {
static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue();
}
return total;
}
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
// fruits.add(42); // would not compile - List<String> only accepts String
Map<String, Integer> prices = new HashMap<>();
prices.put("Apple", 40);
prices.put("Banana", 20);
System.out.println("Fruits: " + fruits);
System.out.println("Apple price: " + prices.get("Apple"));
List<Integer> intList = List.of(10, 20, 30);
System.out.println("Sum of intList: " + sum(intList));
}
}Output
Fruits: [Apple, Banana] Apple price: 40 Sum of intList: 60.0
- ›
List<String>andMap<String, Integer>carry their element types as part of the declaration, so the compiler rejects a wrong-type insertion at compile time instead of failing later with aClassCastException. - ›
prices.get("Apple")returns anIntegerdirectly — no cast needed, because the compiler already knows the map's value type. - ›
sum(List<? extends Number> numbers)uses a wildcard so the same method accepts aList<Integer>, aList<Double>, or any other numeric list, not just one exact type. - ›Inside
sum, each element is read as aNumberand widened withdoubleValue(), which is exactly what the? extends Numberbound guarantees is safe to do.
Generic Concepts in Collections - Quick Reference
| Concept | Where It Appears | Example |
|---|---|---|
Type parameter <E> | Every collection interface and class | List<E>, Set<E>, Queue<E> |
Two type parameters <K,V> | Map types | Map<K,V>, Map.Entry<K,V> |
Bounded <T extends Comparable<T>> | Generic sort/min/max methods | Collections.sort(List<T>) |
Unbounded wildcard ? | Read-only utilities | Collection<?> parameter in frequency() |
Upper wildcard ? extends T | Producer parameters (read from) | addAll(Collection<? extends E>) |
Lower wildcard ? super T | Consumer parameters (write into) | copy(List<? super T> dest, ...) |
| Both wildcards — PECS | Collections.copy() | copy(List<? super T>, List<? extends T>) |
Comparator<? super E> | Sorted collections and sort methods | TreeSet(Comparator<? super E>), sort(Comparator<? super T>) |
Generic Iterator<E> | For-each loop compilation | for (String s : list) |
Diamond <> inference | Every instantiation | new ArrayList<>(), new HashMap<>() |
Best Practices
Always declare collection variables with full type parameters — never raw types. List<String> over List. Map<String, Integer> over Map. Raw types opt out of all compile-time type verification and produce unchecked warnings throughout. Every unchecked warning is a potential runtime ClassCastException that the compiler cannot protect against.
Use the most abstract collection type in method parameters. A method that only iterates should accept Iterable<? extends T> — it works with List, Set, Queue, and any custom iterable. A method that only reads should accept Collection<? extends T>. A method that needs indexed access needs List<T>. The more abstract the parameter, the more call sites the method serves.
Apply PECS to every collection parameter in utility or framework code. For parameters your method reads from, use ? extends T. For parameters your method writes into, use ? super T. For parameters that do both, use a named type parameter <T>. Apply PECS consistently and utility methods become genuinely reusable across the entire type hierarchy they serve.
Return List.copyOf() or Collections.unmodifiableList() from methods that expose internal collections. Both preserve the full generic type — the caller receives List<String>, not List<?> or List<Object>. List.copyOf() is independent of the original; Collections.unmodifiableList() is a live view. Both prevent callers from modifying the internal state through the returned reference.
Common Mistakes
Mistake 1 - Using Raw Collection Types in New Code
import java.util.ArrayList;
import java.util.List;
// WRONG - raw List stores Object, retrieval requires cast,
// wrong-type insertions produce unchecked warnings and ClassCastExceptions
List products = new ArrayList();
products.add("Laptop");
products.add(1299); // silently accepted - different type
String first = (String) products.get(1); // ClassCastException at runtime
// CORRECT - parameterized type enforces the contract at compile time
List<String> productNames = new ArrayList<>();
productNames.add("Laptop");
// productNames.add(1299); // COMPILE ERROR - caught immediately
String firstSafe = productNames.get(0); // no cast - compiler verifiedMistake 2 - Passing List Where List
Practice Programs
Put this topic into practice — each program has full code, output and a step-by-step explanation you can run online.
Creates, modifies, and iterates over an ArrayList.
Stores key-value pairs in a HashMap and looks them up by key.
Adds elements to a HashSet and shows that duplicates are silently ignored.
Sorts a list of objects by one field, then breaks ties using a second field.