Phase 6
Generics & Compile-Time Type Safety
📂 Module 1... \\ Collections part1 demos & Generic Type Safety
6.1 The PECS Principle (Producer Extends, Consumer Super) Explained Simply
📖 Cay S. Horstmann • Core Java Volume I — Chapter 8, Section 8.1 & 8.5 ("Generic Programming & Wildcards")
“Type parameters make programs easier to read and safer to run. Generic types are implemented through type erasure. Remember the PECS rule: Producer Extends, Consumer Super.”
- PECS LawProducer = Extends (read-only); Consumer = Super (write-allowed).
⚡ Pro-Tip: Type Invariance
List<Dog> is NOT a List<Animal>! Generics are invariant to prevent putting Cats into Dog lists.
💡 Real-World Analogy: The Fruit Basket (PECS Decoded)
• Producer Extends (
• Consumer Super (
? extends Fruit): Imagine a basket that produces fruit for you to eat. You can reach in, pull out any item, and safely eat it as a Fruit (Apple, Orange, Banana). But you CANNOT put fruit into the basket! Why? Because the basket might specifically be a List<Apple> — if you tried putting an Orange in, you'd violate type safety!
• Consumer Super (
? super Apple): Imagine a container that consumes apples (you deposit apples into it). It might be an Apple container, a Fruit container, or an Object container. You can safely deposit Apple objects into it all day! But when you read from it, you only get back Object because you don't know the exact parent container type.
📂 GenericsPecsDemo.java — PECS Principle in Action
package com.accenture.ltt.generics;
import java.util.*;
public class GenericsPecsDemo {
// PRODUCER EXTENDS (? extends Number):
// The list PRODUCES numbers for us to read and calculate the sum.
public static double sumOfList(List<? extends Number> list) {
double sum = 0.0;
for (Number n : list) {
sum += n.doubleValue(); // Safe to read!
}
// list.add(10); // COMPILE ERROR: Cannot add to a producer collection!
return sum;
}
// CONSUMER SUPER (? super Integer):
// The list CONSUMES integers we write into it.
public static void addIntegers(List<? super Integer> list) {
list.add(100); // Safe to write!
list.add(200);
// Number n = list.get(0); // COMPILE ERROR: Can only read as Object!
}
}
👁 The PECS Principle (Effective Java Item 31) Breakdown
-
Producer Extends (? extends T)
Lines 6-13: Use when your method reads items out of the collection. You can pass a
List<Integer>,List<Double>, orList<BigDecimal>intosumOfList, and every element is guaranteed to be aNumber. Writing to it is banned by the compiler because Java cannot verify the actual underlying list type. -
Consumer Super (? super T)
Lines 17-21: Use when your method writes items into the collection. You can pass
List<Integer>,List<Number>, orList<Object>, and safely appendIntegerobjects. Reading yields onlyObjectreferences. -
Type Invariance
Remember: In Java,
List<Integer>is NOT a subtype ofList<Number>(Generics are invariant). Wildcards (? extends/? super) provide the covariance and contravariance needed for flexible enterprise APIs.
📂 CollectionsCoreDemo.java — ArrayList, LinkedList, HashSet, TreeSet
import java.util.*;
public class CollectionsCoreDemo {
public static void main(String[] args) {
// 1. ArrayList: O(1) random access, backed by resizable array
List<String> arrayList = new ArrayList<>();
arrayList.add("Java");
arrayList.add("Spring");
System.out.println("ArrayList get(0): " + arrayList.get(0));
// 2. HashSet: O(1) average lookup, eliminates duplicates using HashMap keys
Set<String> hashSet = new HashSet<>();
hashSet.add("Duplicate");
hashSet.add("Duplicate");
System.out.println("HashSet size: " + hashSet.size()); // Prints 1!
// 3. TreeSet: O(log n) lookup, elements always sorted via Red-Black Tree
Set<Integer> treeSet = new TreeSet<>(java.util.List.of(50, 10, 30));
System.out.println("TreeSet automatically sorted: " + treeSet); // [10, 30, 50]
}
}
👁 Core Collections Time Complexities & Memory Footprints
-
ArrayList (O(1) Get / O(N) Insert)
Lines 4-7: Contiguous memory layout. Random access by index (
get(i)) is blazing fast O(1). Inserting into the middle requires shifting subsequent elements (System.arraycopy) taking O(N) time. Grows by 50% (oldCapacity + (oldCapacity >> 1)) when full. -
HashSet (O(1) Average Put/Get)
Lines 10-14: Backed by an internal
HashMapinstance where your elements serve as map keys and a dummy constantPRESENT = new Object()serves as the value. Guarantees uniqueness throughhashCode()andequals(). - TreeSet (O(log N) Sorted Navigable) Lines 17-19: Backed by a Red-Black Tree (self-balancing binary search tree). Elements are kept in sorted order at all times. Search, insertion, and deletion are guaranteed O(log N).
📂 FailFastIteratorDemo.java — How to Avoid ConcurrentModificationException
import java.util.*;
public class FailFastIteratorDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>(java.util.List.of("A", "B", "C"));
// BUG: Modifying collection directly inside foreach loop throws ConcurrentModificationException!
try {
for (String item : list) {
if (item.equals("B")) list.remove(item); // modCount mismatch!
}
} catch (ConcurrentModificationException e) {
System.err.println("Crash: Modifying list during enhanced for-loop!");
}
// SOLUTION: Use explicit Iterator.remove() or Collection.removeIf()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("B")) {
it.remove(); // Safe! Updates expectedModCount cleanly.
}
}
System.out.println("Cleanly modified list: " + list);
}
}
👁 The Fail-Fast Iterator & modCount Internal Trap
-
What Causes CME?
Every collection maintains an internal integer counter called
modCount(modification count). When you calllist.iterator(), the iterator copies this value intoexpectedModCount. -
The modCount Mismatch
In line 8, calling
list.remove(item)incrementsmodCounton the list. But the iterator'sexpectedModCountwas NOT updated. On the next loop pass,it.next()checksif (modCount != expectedModCount)and immediately throwsConcurrentModificationException! -
The Safe Solutions
(1) Use
it.remove(), which updates bothmodCountandexpectedModCountsimultaneously. (2) In Java 8+, uselist.removeIf(item -> item.equals("B")). (3) In multi-threaded systems, useCopyOnWriteArrayListorConcurrentLinkedQueue.
📂 GenericRepositoryDemo.java — Generic Class with Type Bounds
import java.util.*;
// Generic Class with bounded type: ID must be a Number or subclass
public class GenericRepositoryDemo<T, ID extends Number> {
private final Map<ID, T> storage = new HashMap<>();
public void save(ID id, T entity) {
storage.put(id, entity);
}
public Optional<T> findById(ID id) {
return Optional.ofNullable(storage.get(id));
}
// Generic Method with independent type parameter <R>
public <R> List<R> transformAll(java.util.function.Function<T, R> transformer) {
List<R> results = new ArrayList<>();
for (T item : storage.values()) {
results.add(transformer.apply(item));
}
return results;
}
public static void main(String[] args) {
GenericRepositoryDemo<String, Long> repo = new GenericRepositoryDemo<>();
repo.save(1L, "Order-1001");
System.out.println("Found: " + repo.findById(1L).orElse("Not found"));
}
}
👁 Generics Type Bounds & Type Erasure in Production
-
Bounded Type Parameters (<ID extends Number>)
Line 3: Bounding
IDtoNumberguarantees that onlyLong,Integer,Double, etc. can be passed as ID keys. PassingStringorUsertriggers an immediate compile error. -
Generic Method (<R> List<R>)
Line 15: Demonstrates a generic method introducing its own independent type parameter
<R>before the return type. Transforms entities into any caller-specified projection DTO. -
Type Erasure Under the Hood
Java implements generics through Type Erasure for backward compatibility with pre-Java 5 bytecode. At compile time, the compiler strips
<T>and replaces it withObject(or with the upper boundNumberforID), inserting synthetic casts where necessary. At runtime, generic type arguments do not exist in the JVM bytecode!