☕
Core Java Roadmap Phase 06
0 / 40 (0%)
☰ Syllabus Index Next: P07 →

Generics & Compile-Time Type Safety

📂 Repository Module: Module 1_ Collections part1 demos & Generic Type Safety
📖 Textbook Alignment: Volume I — Chapter 8: Generic Programming (Type Parameters, Wildcards, and Erasure)
Phase 6

Generics & Compile-Time Type Safety

📂 Module 1... \\ Collections part1 demos & Generic Type Safety
📖 Core Java Textbook Alignment: Volume I — Chapter 8: Generic Programming (Type Parameters, Wildcards, and Erasure)

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 (? 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>, or List<BigDecimal> into sumOfList, and every element is guaranteed to be a Number. 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>, or List<Object>, and safely append Integer objects. Reading yields only Object references.
  • Type Invariance Remember: In Java, List<Integer> is NOT a subtype of List<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 HashMap instance where your elements serve as map keys and a dummy constant PRESENT = new Object() serves as the value. Guarantees uniqueness through hashCode() and equals().
  • 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 call list.iterator(), the iterator copies this value into expectedModCount.
  • The modCount Mismatch In line 8, calling list.remove(item) increments modCount on the list. But the iterator's expectedModCount was NOT updated. On the next loop pass, it.next() checks if (modCount != expectedModCount) and immediately throws ConcurrentModificationException!
  • The Safe Solutions (1) Use it.remove(), which updates both modCount and expectedModCount simultaneously. (2) In Java 8+, use list.removeIf(item -> item.equals("B")). (3) In multi-threaded systems, use CopyOnWriteArrayList or ConcurrentLinkedQueue.
📂 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 ID to Number guarantees that only Long, Integer, Double, etc. can be passed as ID keys. Passing String or User triggers 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 with Object (or with the upper bound Number for ID), inserting synthetic casts where necessary. At runtime, generic type arguments do not exist in the JVM bytecode!