Java Tutorial
🔍

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.

1// File: ListGenericsDemo.java 2 3import java.util.*; 4import java.util.function.*; 5 6public class ListGenericsDemo { 7 8 // Generic method with bounded param - sorts any Comparable list 9 static <T extends Comparable<T>> List<T> sortedCopy(List<T> source) { 10 List<T> copy = new ArrayList<>(source); 11 Collections.sort(copy); 12 return copy; 13 } 14 15 // PECS in a custom utility - reads from producer, writes to consumer 16 static <T> void copyIf(List<? extends T> source, 17 List<? super T> dest, 18 Predicate<? super T> predicate) { 19 for (T item : source) { // source PRODUCES T (? extends) 20 if (predicate.test(item)) { 21 dest.add(item); // dest CONSUMES T (? super) 22 } 23 } 24 } 25 26 public static void main(String[] args) { 27 28 List<String> products = new ArrayList<>( 29 List.of("USB Hub", "Wireless Mouse", "Keyboard", "Laptop Stand", "Webcam")); 30 31 System.out.println("=== sortedCopy - bounded <T extends Comparable<T>> ==="); 32 List<String> sorted = sortedCopy(products); 33 System.out.println("Original : " + products); 34 System.out.println("Sorted : " + sorted); 35 36 System.out.println(); 37 38 System.out.println("=== sortedCopy - same method with List<Integer> ==="); 39 List<Integer> prices = new ArrayList<>(List.of(1299, 499, 2499, 799, 3499)); 40 List<Integer> sortedPrices = sortedCopy(prices); 41 System.out.println("Sorted prices: " + sortedPrices); 42 43 System.out.println(); 44 45 System.out.println("=== copyIf - PECS: ? extends source, ? super dest ==="); 46 List<Object> destObj = new ArrayList<>(); // List<Object> satisfies ? super String 47 copyIf(products, destObj, name -> name.length() > 7); 48 System.out.println("Names longer than 7 chars (into List<Object>): " + destObj); 49 50 System.out.println(); 51 52 System.out.println("=== for-each uses Iterator<E> - no cast in sight ==="); 53 int total = 0; 54 for (int price : prices) { // Integer unboxed to int - no explicit cast 55 total += price; 56 } 57 System.out.println("Total: Rs." + total); 58 59 System.out.println(); 60 61 System.out.println("=== Collections.addAll uses ? super T internally ==="); 62 List<Number> numList = new ArrayList<>(); 63 Collections.addAll(numList, 10, 20, 30); // Integer added - Number satisfies ? super Integer 64 System.out.println("numList: " + numList); 65 } 66}
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.

1// File: MapGenericsDemo.java 2 3import java.util.*; 4import java.util.function.*; 5 6public class MapGenericsDemo { 7 8 // Inverts Map<K,V> to Map<V,K> - uses Map.Entry<K,V> to iterate 9 static <K, V> Map<V, K> invert(Map<K, V> source) { 10 Map<V, K> result = new LinkedHashMap<>(); 11 for (Map.Entry<K, V> entry : source.entrySet()) { 12 K key = entry.getKey(); // K - typed, no cast 13 V value = entry.getValue(); // V - typed, no cast 14 result.put(value, key); 15 } 16 return result; 17 } 18 19 // Groups items by a key function - List<T> -> Map<K, List<T>> 20 static <T, K> Map<K, List<T>> groupBy(List<T> items, Function<T, K> keyFn) { 21 Map<K, List<T>> groups = new LinkedHashMap<>(); 22 for (T item : items) { 23 groups.computeIfAbsent(keyFn.apply(item), k -> new ArrayList<>()).add(item); 24 } 25 return groups; 26 } 27 28 // Merges two maps using ? extends K and ? extends V - accepts any subtype maps 29 static <K, V> Map<K, V> merge(Map<? extends K, ? extends V> first, 30 Map<? extends K, ? extends V> second) { 31 Map<K, V> result = new LinkedHashMap<>(first); 32 result.putAll(second); // putAll accepts Map<? extends K, ? extends V> 33 return result; 34 } 35 36 public static void main(String[] args) { 37 38 Map<String, Integer> stockLevels = new LinkedHashMap<>(); 39 stockLevels.put("P001-Mouse", 120); 40 stockLevels.put("P002-Stand", 45); 41 stockLevels.put("P003-USBHub", 200); 42 stockLevels.put("P004-Keyboard", 28); 43 44 System.out.println("=== Map.Entry<K,V> - both key and value fully typed ==="); 45 for (Map.Entry<String, Integer> entry : stockLevels.entrySet()) { 46 String id = entry.getKey(); // String - no cast 47 Integer qty = entry.getValue(); // Integer - no cast 48 String status = qty < 50 ? "LOW" : "OK"; 49 System.out.printf(" %-22s qty=%-4d [%s]%n", id, qty, status); 50 } 51 52 System.out.println(); 53 54 System.out.println("=== invert - Map<String,Integer> to Map<Integer,String> ==="); 55 Map<Integer, String> inverted = invert(stockLevels); 56 inverted.forEach((qty, id) -> System.out.printf(" qty=%-4d -> %s%n", qty, id)); 57 58 System.out.println(); 59 60 System.out.println("=== groupBy - group product IDs by stock status ==="); 61 List<String> ids = List.of("P001-Mouse", "P002-Stand", "P003-USBHub", "P004-Keyboard"); 62 Map<String, List<String>> byStatus = groupBy( 63 ids, id -> stockLevels.getOrDefault(id, 0) >= 50 ? "IN_STOCK" : "LOW_STOCK"); 64 byStatus.forEach((status, list) -> 65 System.out.println(" " + status + ": " + list)); 66 67 System.out.println(); 68 69 System.out.println("=== merge two maps with ? extends K, ? extends V ==="); 70 Map<String, Integer> warehouseA = Map.of("P001-Mouse", 60, "P003-USBHub", 100); 71 Map<String, Integer> warehouseB = Map.of("P002-Stand", 30, "P004-Keyboard", 15); 72 Map<String, Integer> combined = merge(warehouseA, warehouseB); 73 System.out.println(" Combined: " + combined); 74 } 75}
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>.

1// File: SetGenericsDemo.java 2 3import java.util.*; 4 5public class SetGenericsDemo { 6 7 record Product(String id, String name, double price) {} 8 9 public static void main(String[] args) { 10 11 System.out.println("=== HashSet<String> - type-safe, no duplicates ==="); 12 Set<String> categories = new HashSet<>(); 13 categories.add("Electronics"); 14 categories.add("Accessories"); 15 categories.add("Electronics"); // silently deduplicated 16 categories.add("Furniture"); 17 System.out.println("Categories : " + categories); 18 System.out.println("Has Electronics: " + categories.contains("Electronics")); 19 20 System.out.println(); 21 22 System.out.println("=== TreeSet<String> - natural order via Comparable<String> ==="); 23 TreeSet<String> sorted = new TreeSet<>(categories); 24 System.out.println("Sorted : " + sorted); 25 System.out.println("First : " + sorted.first()); 26 System.out.println("Last : " + sorted.last()); 27 System.out.println("headSet(<F) : " + sorted.headSet("F")); 28 29 System.out.println(); 30 31 System.out.println("=== TreeSet with Comparator<? super Product> ==="); 32 // Product does NOT implement Comparable - Comparator is required 33 // Comparator<Product> satisfies Comparator<? super Product> 34 Comparator<Product> byPrice = Comparator.comparingDouble(Product::price); 35 36 TreeSet<Product> byPriceSet = new TreeSet<>(byPrice); 37 byPriceSet.add(new Product("P001", "Wireless Mouse", 799.0)); 38 byPriceSet.add(new Product("P002", "Laptop Stand", 1299.0)); 39 byPriceSet.add(new Product("P003", "USB Hub", 499.0)); 40 byPriceSet.add(new Product("P004", "Keyboard", 2499.0)); 41 42 System.out.println("Products sorted by price:"); 43 byPriceSet.forEach(p -> 44 System.out.printf(" %-20s Rs.%.2f%n", p.name(), p.price())); 45 46 System.out.println(); 47 48 System.out.println("=== Set operations - addAll / retainAll use ? extends E / ? ==="); 49 Set<Integer> setA = new HashSet<>(Set.of(499, 799, 1299, 2499)); 50 Set<Integer> setB = new HashSet<>(Set.of(799, 1299, 3499)); 51 52 Set<Integer> union = new HashSet<>(setA); 53 union.addAll(setB); // addAll accepts Collection<? extends Integer> 54 System.out.println("Union : " + new TreeSet<>(union)); 55 56 Set<Integer> intersection = new HashSet<>(setA); 57 intersection.retainAll(setB); // retainAll accepts Collection<?> 58 System.out.println("Intersection: " + new TreeSet<>(intersection)); 59 } 60}
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.

1// File: CollectionsUtilityDemo.java 2 3import java.util.*; 4 5public class CollectionsUtilityDemo { 6 7 public static void main(String[] args) { 8 9 System.out.println("=== Collections.sort with Comparator<? super T> ==="); 10 // sort(List<T> list, Comparator<? super T> c) 11 // Comparator<Object> satisfies Comparator<? super String> 12 List<String> items = new ArrayList<>( 13 List.of("Webcam", "Keyboard", "Hub", "Mouse", "Monitor")); 14 15 Collections.sort(items); // natural order 16 System.out.println("Natural : " + items); 17 18 Collections.sort(items, Comparator.comparingInt(String::length)); 19 System.out.println("By length: " + items); 20 21 System.out.println(); 22 23 System.out.println("=== Collections.copy - PECS in one signature ==="); 24 // copy(List<? super T> dest, List<? extends T> src) 25 // src PRODUCES T values -> ? extends T (producer) 26 // dest CONSUMES T values -> ? super T (consumer) 27 List<Integer> source = List.of(499, 799, 1299, 2499); 28 List<Number> dest = new ArrayList<>(Arrays.asList(0, 0, 0, 0)); 29 30 Collections.copy(dest, source); 31 // List<Number> satisfies ? super Integer (Number IS a supertype of Integer) 32 System.out.println("Copied into List<Number>: " + dest); 33 34 System.out.println(); 35 36 System.out.println("=== Collections.min / max - Comparable or Comparator<? super T> ==="); 37 List<Integer> scores = List.of(72, 95, 88, 64, 91, 79); 38 System.out.println("Min: " + Collections.min(scores)); 39 System.out.println("Max: " + Collections.max(scores)); 40 41 System.out.println(); 42 43 System.out.println("=== Collections.frequency - Object parameter for backwards compat ==="); 44 List<String> tags = List.of("sale", "new", "sale", "trending", "new", "sale"); 45 System.out.println("'sale' count : " + Collections.frequency(tags, "sale")); 46 System.out.println("'trending' count: " + Collections.frequency(tags, "trending")); 47 48 System.out.println(); 49 50 System.out.println("=== Collections.unmodifiableList - preserves type parameter ==="); 51 List<String> mutable = new ArrayList<>(List.of("Alpha", "Beta", "Gamma")); 52 List<String> immutable = Collections.unmodifiableList(mutable); 53 54 String first = immutable.get(0); // String - type parameter preserved 55 System.out.println("get(0) as String: " + first.toUpperCase()); 56 57 try { 58 immutable.add("Delta"); 59 } catch (UnsupportedOperationException e) { 60 System.out.println("add() blocked: UnsupportedOperationException"); 61 } 62 } 63}
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.

1// File: IteratorGenericsDemo.java 2 3import java.util.*; 4import java.util.function.*; 5 6public class IteratorGenericsDemo { 7 8 // Custom generic Iterable - implements Iterable<T> for any type T 9 static class FilteredIterable<T> implements Iterable<T> { 10 private final List<T> source; 11 private final Predicate<T> filter; 12 13 FilteredIterable(List<T> source, Predicate<T> filter) { 14 this.source = List.copyOf(source); 15 this.filter = filter; 16 } 17 18 @Override 19 public Iterator<T> iterator() { 20 return new Iterator<T>() { 21 private int index = 0; 22 private T pending; 23 private boolean ready = false; 24 25 private boolean advance() { 26 while (index < source.size()) { 27 T candidate = source.get(index++); 28 if (filter.test(candidate)) { 29 pending = candidate; 30 ready = true; 31 return true; 32 } 33 } 34 ready = false; 35 return false; 36 } 37 38 @Override public boolean hasNext() { return ready || advance(); } 39 40 @Override 41 public T next() { 42 if (!ready && !advance()) throw new NoSuchElementException(); 43 ready = false; 44 return pending; // returns T - fully typed 45 } 46 }; 47 } 48 } 49 50 public static void main(String[] args) { 51 52 System.out.println("=== Manual Iterator<String> - next() returns String ==="); 53 List<String> cities = List.of("Mumbai", "Pune", "Nashik", "Nagpur", "Aurangabad"); 54 Iterator<String> it = cities.iterator(); 55 while (it.hasNext()) { 56 String city = it.next(); // String - no cast 57 System.out.println(" " + city.toUpperCase()); 58 } 59 60 System.out.println(); 61 62 System.out.println("=== for-each compiles to the same Iterator<String> loop ==="); 63 for (String city : cities) { // identical bytecode to above 64 System.out.println(" " + city.length() + " chars"); 65 } 66 67 System.out.println(); 68 69 System.out.println("=== Custom FilteredIterable<Integer> - only prices > 1000 ==="); 70 List<Integer> allPrices = List.of(499, 1299, 799, 2499, 399, 1799, 649); 71 FilteredIterable<Integer> expensive = 72 new FilteredIterable<>(allPrices, price -> price > 1000); 73 74 System.out.print("Expensive: "); 75 for (Integer price : expensive) { // for-each uses our generic Iterator<Integer> 76 System.out.print("Rs." + price + " "); 77 } 78 System.out.println(); 79 80 System.out.println(); 81 82 System.out.println("=== Safe removal via Iterator.remove() ==="); 83 List<Integer> mutablePrices = new ArrayList<>( 84 List.of(499, 1299, 799, 2499, 399, 1799)); 85 Iterator<Integer> remover = mutablePrices.iterator(); 86 while (remover.hasNext()) { 87 Integer price = remover.next(); // Integer - typed by Iterator<Integer> 88 if (price < 1000) remover.remove(); // safe in-loop removal 89 } 90 System.out.println("Remaining (>= Rs.1000): " + mutablePrices); 91 } 92}
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]

Real-World Example - Flipkart Inventory Store and Analytics

A multi-warehouse inventory system stores product stock by warehouse, computes cross-warehouse totals, generates low-stock alerts with a configurable sort order, and groups products by category. Every generic concept from the series appears: nested Map<K, Map<K,V>>, List<? extends Number> for summing, Comparator<? super Product> for flexible sorting, Set<String> with TreeSet for sorted deduplication, and Collections.unmodifiableMap preserving the type parameter through the return path.

1// File: InventoryStore.java 2 3import java.util.*; 4 5public class InventoryStore { 6 7 // Nested generic map: warehouseId -> productId -> quantity 8 private final Map<String, Map<String, Integer>> data = new LinkedHashMap<>(); 9 10 public void setStock(String warehouse, String productId, int qty) { 11 data.computeIfAbsent(warehouse, k -> new LinkedHashMap<>()) 12 .put(productId, qty); 13 } 14 15 public int getStock(String warehouse, String productId) { 16 return data.getOrDefault(warehouse, Map.of()).getOrDefault(productId, 0); 17 } 18 19 public Map<String, Integer> warehouseView(String warehouse) { 20 // Returns unmodifiable Map<String,Integer> - type parameter preserved 21 return Collections.unmodifiableMap( 22 data.getOrDefault(warehouse, Map.of())); 23 } 24 25 // Set<String> - TreeSet guarantees sorted order and deduplication 26 public Set<String> allProductIds() { 27 Set<String> ids = new TreeSet<>(); 28 data.values().forEach(m -> ids.addAll(m.keySet())); 29 return Collections.unmodifiableSet(ids); 30 } 31 32 // List<Integer> per product - one entry per warehouse holding that product 33 public List<Integer> stockAcrossWarehouses(String productId) { 34 List<Integer> quantities = new ArrayList<>(); 35 for (Map<String, Integer> warehouseMap : data.values()) { 36 int qty = warehouseMap.getOrDefault(productId, 0); 37 if (qty > 0) quantities.add(qty); 38 } 39 return List.copyOf(quantities); 40 } 41 42 // List<? extends Number> - reads any numeric list using doubleValue() 43 public static double sumStock(List<? extends Number> quantities) { 44 double total = 0; 45 for (Number n : quantities) { // Number - ? extends Number enables this 46 total += n.doubleValue(); 47 } 48 return total; 49 } 50}
1// File: InventoryAnalytics.java 2 3import java.util.*; 4 5public class InventoryAnalytics { 6 7 private final InventoryStore store; 8 private final Map<String, String> productNames; 9 private final Map<String, String> productCategories; 10 private final Map<String, Double> unitPrices; 11 12 public InventoryAnalytics(InventoryStore store, 13 Map<String, String> productNames, 14 Map<String, String> productCategories, 15 Map<String, Double> unitPrices) { 16 this.store = store; 17 this.productNames = Map.copyOf(productNames); 18 this.productCategories = Map.copyOf(productCategories); 19 this.unitPrices = Map.copyOf(unitPrices); 20 } 21 22 // Low-stock: total below threshold, sorted by Comparator<? super String> 23 // Comparator operates on productId strings - ? super String accepted 24 public List<String> lowStockProducts(int threshold, 25 Comparator<? super String> sortOrder) { 26 List<String> alerts = new ArrayList<>(); 27 for (String productId : store.allProductIds()) { 28 double total = InventoryStore.sumStock(store.stockAcrossWarehouses(productId)); 29 if (total < threshold) alerts.add(productId); 30 } 31 alerts.sort(sortOrder); // Comparator<? super String> 32 return List.copyOf(alerts); 33 } 34 35 // Total stock value per product: Map<productId, totalValue> 36 public Map<String, Double> stockValueMap() { 37 Map<String, Double> result = new LinkedHashMap<>(); 38 for (String productId : store.allProductIds()) { 39 double totalQty = InventoryStore.sumStock(store.stockAcrossWarehouses(productId)); 40 double price = unitPrices.getOrDefault(productId, 0.0); 41 result.put(productId, totalQty * price); 42 } 43 return Collections.unmodifiableMap(result); 44 } 45 46 // Group productIds by category: Map<category, List<productId>> 47 public Map<String, List<String>> byCategory() { 48 Map<String, List<String>> groups = new TreeMap<>(); // TreeMap sorts categories 49 for (String productId : store.allProductIds()) { 50 String category = productCategories.getOrDefault(productId, "Uncategorised"); 51 groups.computeIfAbsent(category, k -> new ArrayList<>()).add(productId); 52 } 53 return Collections.unmodifiableMap(groups); 54 } 55}
1// File: FlipkartInventoryDemo.java 2 3import java.util.*; 4 5public class FlipkartInventoryDemo { 6 7 public static void main(String[] args) { 8 9 InventoryStore store = new InventoryStore(); 10 11 store.setStock("WH-NORTH", "P001", 120); 12 store.setStock("WH-NORTH", "P002", 15); 13 store.setStock("WH-NORTH", "P003", 200); 14 store.setStock("WH-NORTH", "P004", 8); 15 store.setStock("WH-NORTH", "P005", 350); 16 17 store.setStock("WH-SOUTH", "P001", 60); 18 store.setStock("WH-SOUTH", "P002", 25); 19 store.setStock("WH-SOUTH", "P003", 5); 20 store.setStock("WH-SOUTH", "P004", 20); 21 22 Map<String, String> names = Map.of( 23 "P001", "Wireless Mouse", "P002", "Laptop Stand", 24 "P003", "USB Hub", "P004", "Keyboard", 25 "P005", "Cable Organiser"); 26 27 Map<String, String> categories = Map.of( 28 "P001", "Electronics", "P002", "Accessories", 29 "P003", "Electronics", "P004", "Electronics", 30 "P005", "Accessories"); 31 32 Map<String, Double> prices = Map.of( 33 "P001", 799.0, "P002", 1299.0, 34 "P003", 499.0, "P004", 2499.0, 35 "P005", 299.0); 36 37 InventoryAnalytics analytics = new InventoryAnalytics( 38 store, names, categories, prices); 39 40 System.out.println("=== All product IDs (TreeSet - sorted, deduplicated) ==="); 41 System.out.println(store.allProductIds()); 42 43 System.out.println(); 44 45 System.out.println("=== Stock across warehouses (List<? extends Number> summed) ==="); 46 for (String id : store.allProductIds()) { 47 List<Integer> perWarehouse = store.stockAcrossWarehouses(id); 48 double total = InventoryStore.sumStock(perWarehouse); 49 System.out.printf(" %-5s %-20s per-warehouse=%s total=%.0f%n", 50 id, names.get(id), perWarehouse, total); 51 } 52 53 System.out.println(); 54 55 System.out.println("=== Low stock alerts - threshold 50, sorted by natural order ==="); 56 // Comparator.naturalOrder() returns Comparator<String> 57 // satisfies Comparator<? super String> 58 List<String> alerts = analytics.lowStockProducts(50, Comparator.naturalOrder()); 59 alerts.forEach(id -> 60 System.out.printf(" ALERT: %-5s %-20s total=%.0f%n", 61 id, names.get(id), 62 InventoryStore.sumStock(store.stockAcrossWarehouses(id)))); 63 64 System.out.println(); 65 66 System.out.println("=== Low stock sorted by reverse name length (Comparator<Object>) ==="); 67 // Comparator<Object> satisfies Comparator<? super String> 68 Comparator<Object> byNameLengthDesc = 69 Comparator.comparing(obj -> -((String) obj).length()); 70 List<String> alertsByLength = analytics.lowStockProducts(50, byNameLengthDesc); 71 alertsByLength.forEach(id -> System.out.println(" " + names.get(id))); 72 73 System.out.println(); 74 75 System.out.println("=== Stock value per product (Map<String,Double>) ==="); 76 analytics.stockValueMap().forEach((id, value) -> 77 System.out.printf(" %-5s %-20s value=Rs.%,.2f%n", 78 id, names.get(id), value)); 79 80 System.out.println(); 81 82 System.out.println("=== Products by category (TreeMap<String,List<String>>) ==="); 83 analytics.byCategory().forEach((category, ids) -> { 84 System.out.println(" " + category + ":"); 85 ids.forEach(id -> System.out.println(" " + id + " - " + names.get(id))); 86 }); 87 } 88}
Output:
=== All product IDs (TreeSet - sorted, deduplicated) ===
[P001, P002, P003, P004, P005]

=== Stock across warehouses (List<? extends Number> summed) ===
  P001  Wireless Mouse       per-warehouse=[120, 60]   total=180
  P002  Laptop Stand         per-warehouse=[15, 25]    total=40
  P003  USB Hub              per-warehouse=[200, 5]    total=205
  P004  Keyboard             per-warehouse=[8, 20]     total=28
  P005  Cable Organiser      per-warehouse=[350]       total=350

=== Low stock alerts - threshold 50, sorted by natural order ===
  ALERT: P002  Laptop Stand         total=40
  ALERT: P004  Keyboard             total=28

=== Low stock sorted by reverse name length (Comparator<Object>) ===
  Laptop Stand
  Keyboard

=== Stock value per product (Map<String,Double>) ===
  P001  Wireless Mouse       value=Rs.143,820.00
  P002  Laptop Stand         value=Rs.51,960.00
  P003  USB Hub              value=Rs.102,295.00
  P004  Keyboard             value=Rs.69,972.00
  P005  Cable Organiser      value=Rs.104,650.00

=== Products by category (TreeMap<String,List<String>>) ===
  Accessories:
    P002 - Laptop Stand
    P005 - Cable Organiser
  Electronics:
    P001 - Wireless Mouse
    P003 - USB Hub
    P004 - Keyboard

Every concept from the series appears in this one example. Map<String, Map<String, Integer>> is nested generic typing. Set<String> with TreeSet gives sorted deduplication across warehouses. List<? extends Number> in sumStock() reads from any numeric list without code duplication. Comparator<? super String> in lowStockProducts() accepts both specific and general comparators. TreeMap<String, List<String>> in byCategory() keeps categories alphabetically ordered. Collections.unmodifiableMap() and List.copyOf() preserve type parameters through the return boundary.

Generic Concepts in Collections - Quick Reference

ConceptWhere It AppearsExample
Type parameter <E>Every collection interface and classList<E>, Set<E>, Queue<E>
Two type parameters <K,V>Map typesMap<K,V>, Map.Entry<K,V>
Bounded <T extends Comparable<T>>Generic sort/min/max methodsCollections.sort(List<T>)
Unbounded wildcard ?Read-only utilitiesCollection<?> parameter in frequency()
Upper wildcard ? extends TProducer parameters (read from)addAll(Collection<? extends E>)
Lower wildcard ? super TConsumer parameters (write into)copy(List<? super T> dest, ...)
Both wildcards — PECSCollections.copy()copy(List<? super T>, List<? extends T>)
Comparator<? super E>Sorted collections and sort methodsTreeSet(Comparator<? super E>), sort(Comparator<? super T>)
Generic Iterator<E>For-each loop compilationfor (String s : list)
Diamond <> inferenceEvery instantiationnew 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

1import java.util.ArrayList; 2import java.util.List; 3 4// WRONG - raw List stores Object, retrieval requires cast, 5// wrong-type insertions produce unchecked warnings and ClassCastExceptions 6List products = new ArrayList(); 7products.add("Laptop"); 8products.add(1299); // silently accepted - different type 9String first = (String) products.get(1); // ClassCastException at runtime 10 11// CORRECT - parameterized type enforces the contract at compile time 12List<String> productNames = new ArrayList<>(); 13productNames.add("Laptop"); 14// productNames.add(1299); // COMPILE ERROR - caught immediately 15String firstSafe = productNames.get(0); // no cast - compiler verified

Mistake 2 - Passing List Where List Is Expected
1import java.util.List; 2 3// WRONG - List<Object> only accepts an actual List<Object> due to invariance. 4// In a well-typed codebase, List<Object> is almost never created. 5// This method is unusable with List<String>, List<Integer>, etc. 6static void logAllBroken(List<Object> items) { 7 items.forEach(System.out::println); 8} 9 10List<String> names = List.of("Ananya", "Priya"); 11// logAllBroken(names); // COMPILE ERROR 12 13// CORRECT - wildcard makes the method accept any parameterized List 14static void logAll(List<?> items) { 15 items.forEach(System.out::println); 16} 17// logAll(names); // works - ? accepts List<String>

Mistake 3 - Modifying a Collection During for-each Iteration

1import java.util.ArrayList; 2import java.util.Iterator; 3import java.util.List; 4 5// WRONG - structural modification during for-each (which uses Iterator internally) 6// throws ConcurrentModificationException 7static void removeLowStockBroken(List<Integer> quantities) { 8 for (Integer qty : quantities) { 9 if (qty < 50) quantities.remove(qty); // ConcurrentModificationException 10 } 11} 12 13// CORRECT - use Iterator<T>.remove() which is safe during iteration 14static void removeLowStock(List<Integer> quantities) { 15 Iterator<Integer> it = quantities.iterator(); 16 while (it.hasNext()) { 17 Integer qty = it.next(); // Integer - typed by Iterator<Integer> 18 if (qty < 50) it.remove(); // safe in-loop removal via iterator 19 } 20}

Mistake 4 - Declaring ? extends T When ? super T Is Needed (Wrong PECS Side)

1import java.util.List; 2 3// WRONG - ? extends T is for reading (producer side). 4// This method needs to WRITE integers into dest - that requires ? super T. 5// ? extends prevents add() - the method body cannot compile. 6static void fillBroken(List<? extends Integer> dest, int value) { 7 // dest.add(value); // COMPILE ERROR - cannot add to ? extends 8} 9 10// CORRECT - ? super T is for writing (consumer side) 11// dest can be List<Integer>, List<Number>, or List<Object> 12static void fill(List<? super Integer> dest, int value, int times) { 13 for (int i = 0; i < times; i++) { 14 dest.add(value); // Integer into ? super Integer - safe 15 } 16}

Interview Questions

Q1. How do generics improve the Collections Framework compared to pre-Java-5 code?

Before Java 5, all collections stored Object references — add() accepted anything, get() returned Object requiring a downcast, and type mismatches caused ClassCastException at runtime often far from the actual mistake. Generics parameterize every collection — List<E>, Map<K,V>, Set<E> — making the element type part of the compile-time contract. The compiler verifies every insertion and retrieval, rejects type mismatches as errors before the code can run, and inserts automatic casts in bytecode that it has already verified are correct. The practical shift is from runtime bugs to compile-time errors.

Q2. Why can't a List be passed where a List is expected?

Generic types are invariant in Java — List<String> is not a subtype of List<Object> even though String is a subtype of Object. The invariance rule exists for type safety: if List<String> were a List<Object>, a method holding a List<Object> reference could call add(42) on what is actually a List<String>, silently corrupting it. The solution is the unbounded wildcard: List<?> accepts any parameterized list for read-only structural operations, and List<? extends String> accepts any subtype list for typed reading.

Q3. What does Collections.copy(List<? super T> dest, List<? extends T> src) demonstrate?

This signature demonstrates PECS — Producer Extends, Consumer Super — in the most complete form in the JDK. src is a producer: the method reads T values from it, so it uses ? extends T. dest is a consumer: the method writes T values into it, so it uses ? super T. The ? super T on dest allows a List<Number> to receive Integer values when T = Integer, because Number is a supertype of Integer. A simpler declaration copy(List<T>, List<T>) would force both lists to be exactly the same parameterized type, rejecting the List<Number> destination.

Q4. How does TreeSet use generics differently from HashSet?

Both TreeSet<E> and HashSet<E> use a single element type parameter E. The difference is the ordering contract. HashSet uses hashCode() and equals() from Object and requires nothing special of E. TreeSet maintains sorted order, which requires either that E implements Comparable<? super E> (natural ordering — a violation throws ClassCastException at the first insertion) or that a Comparator<? super E> is provided at construction. The Comparator<? super E> in TreeSet's constructor is the PECS consumer pattern — the comparator consumes E values for comparison, so ? super E allows general-purpose comparators to be passed.

Q5. How does the for-each loop connect to Java generics?

for (String s : list) compiles to a while loop using list.iterator(). Because List<String> implements Iterable<String>, iterator() returns Iterator<String>, whose next() method returns String directly. The compiler inserts a CHECKCAST to String in the bytecode — verified correct because the list was typed — and no explicit cast is written in application code. Before generics, using an iterator required (String) it.next() at every call site, with no compile-time guarantee the cast would succeed.

Q6. What is the correct way to remove elements from a List during iteration?

The for-each loop internally uses an Iterator. Calling the list's own remove() method inside a for-each loop structurally modifies the list while the iterator is active, which triggers ConcurrentModificationException. The correct approach is to obtain the iterator explicitly — Iterator<T> it = list.iterator() — and call it.remove() rather than list.remove(). The Iterator.remove() method is designed for safe in-loop removal: it removes the element last returned by next() and updates the iterator's internal state so iteration continues correctly. The element type T from Iterator<T> ensures it.next() returns a typed value without a cast.

FAQs

Why do contains() and remove() on List accept Object instead of E?

For backward compatibility with pre-Java-5 code. Before generics, contains(Object) was the only option. Changing it to contains(E) would break existing code that passed non-E arguments — which always return false rather than failing. The designers preserved the Object signature in these read-only query methods. This means list.contains(42) on a List<String> compiles and returns false rather than being rejected, which is occasionally surprising but rarely harmful.

Is there a performance difference between generic and raw collections?

No. After type erasure, the compiled bytecode for List<String> and a raw List is essentially identical — both use Object internally. The automatic CHECKCAST instructions the compiler inserts at typed read sites are the same bytecode an explicit cast would produce. The JIT applies the same optimizations to both. The one performance concern with generic collections is boxing: List<Integer> boxes int values to Integer objects. That cost comes from boxing, not from generics.

Can you have a Map whose keys are generic?

Yes. Map<List<String>, Integer> uses a List<String> as the key type. The key type must correctly implement hashCode() and equals() for HashMap (or Comparable for TreeMap). List does implement both — hashCode() is based on element hash codes, equals() compares element by element. Using mutable objects as HashMap keys is risky regardless of generics: if the key's state changes after insertion, get() will fail to find it.

What is the difference between List.copyOf() and Collections.unmodifiableList()?

Both return an unmodifiable List<E> that preserves the type parameter. The difference is independence: List.copyOf(source) creates a completely independent copy — subsequent changes to source do not affect the copy. Collections.unmodifiableList(source) returns a view — changes to source are visible through the view. For encapsulation, List.copyOf() is usually preferred when returning an internal collection from a method; the caller cannot observe internal state changes through their copy.

Why does addAll() use Collection<? extends E> instead of Collection?

addAll(Collection<? extends E> c) uses the PECS producer pattern: the argument collection produces E values that addAll() reads and inserts. The ? extends E allows passing a Collection<String> to a List<CharSequence>.addAll() call — because String is a subtype of CharSequence. Without the wildcard, addAll() on a List<CharSequence> would reject any Collection<String> due to generic invariance, making the method far less useful for real-world collection manipulation.

Can you create a generic array, like new ArrayList[10]?

No — generic array creation is a compile error. Arrays are covariant and reifiable at runtime, while generic types are invariant and erased. Mixing them creates potential heap pollution: an ArrayList<String>[] reference could point to ArrayList<Integer> elements through array covariance, circumventing the type system. The standard workaround is new ArrayList[10] (raw array, suppressed unchecked warning) or, preferably, using a List<List<String>> instead of an array of lists. This limitation is a consequence of type erasure and is explained in the Type Erasure article in this series.

Summary

Generics are not a layer added on top of the Collections Framework — they are structurally woven through every interface, every class, every utility method. List<E> makes insertions and retrievals type-safe. Map<K,V> gives both the key and value their own independent type. Set<E> plus TreeSet's Comparator<? super E> demonstrates PECS at the constructor level. Collections.copy(List<? super T> dest, List<? extends T> src) is the textbook PECS example with both sides in one signature. Iterator<E> powers the for-each loop so elements come out typed without a cast.

Every pattern from the Generics series has a home in collections: bounded type parameters in sort and min and max; unbounded wildcards in frequency and contains; upper-bounded wildcards in addAll and copyOf; lower-bounded wildcards in copy destination and Comparator parameters; generic iterators behind every for-each loop.

Writing List<Product> instead of raw List is not ceremonial. It is the decision that moves an entire class of bugs from production ClassCastException to compile-time error — caught in seconds rather than discovered in hours.

What to Read Next