🃏 Generics: Bounded Wildcards (PECS Rule)

PECS Rule: Producer Extends, Consumer Super

  • ? extends T: READ-ONLY - Can read items of type T or its subtypes. Cannot add anything (except null)
  • ? super T: WRITE-ONLY - Can write T or its subtypes. Cannot safely read (except Object)
// Producer Extends - Reading from a collection
List<? extends Number> numbers = List.of(1, 2.0, 3L);
Number n = numbers.get(0);    // ✅ OK - can read as Number
// numbers.add(3);            // ❌ Compile error - cannot write

// Consumer Super - Writing to a collection  
List<? super Integer> values = new ArrayList<Number>();
values.add(10);       // ✅ OK - can write Integer/subtypes
values.add(42);       // ✅ OK - can write Integer/subtypes
// Integer i = values.get(0);  // ❌ Compile error - can only read as Object
Object obj = values.get(0);   // ✅ OK - can read as Object

💡 Learning Tip: Think of wildcards as “one-way streets” - extends for reading OUT, super for writing IN.

🃏 Deque Stack vs Queue Operations

Rule: Deque can act as both Stack (LIFO) and Queue (FIFO) with different method behaviors.

  • Stack operations: push() and pop() work at the front/head (LIFO - Last In First Out).
  • Queue operations: offer()/add() at tail, poll()/remove() at head (FIFO - First In First Out).
  • Mixed usage can cause confusion - know which end each method operates on.
public class FamilyLineup {
    public static void main(String[] args) {
        Deque<String> familyLine = new ArrayDeque<>();
        
        // Using Stack operations (all work at FRONT/HEAD)
        familyLine.push("Father");    // [Father]
        familyLine.push("Mother");    // [Mother, Father] - Mother at front
        familyLine.push("Child");     // [Child, Mother, Father] - Child at front
        
        // Mixed operations - be careful!
        System.out.println(familyLine.pollFirst());  // Child (removes from front/head)
        System.out.println(familyLine.poll());       // Mother (poll() = pollFirst(), removes from front/head)
        System.out.println(familyLine.pollLast());   // Father (removes from back/tail)
        
        // Output:
        // Child
        // Mother  
        // Father
    }
}

// Stack view: [Child, Mother, Father] (Child is top/front)
// Queue view: [Child, Mother, Father] (Child is head, Father is tail)

💡 Learning Tip: Remember “STACK FRONT, QUEUE ENDS” - Stack operations (push/pop) work at front only, Queue operations work at opposite ends (add tail, remove head).

Q: If you push three elements then call pollFirst(), poll(), and pollLast(), what’s the removal order?
A: First element pushed, second element pushed, third element pushed - because pollFirst() and poll() both remove from head, pollLast() from tail.

🃏 Set Operations and Characteristics

Rule: Set implementations have different ordering and performance characteristics.

  • HashSet: No ordering, O(1) operations, allows null
  • LinkedHashSet: Insertion order, O(1) operations, allows null
  • TreeSet: Natural/comparator ordering, O(log n) operations, no null
// HashSet - no ordering guaranteed
Set<String> hashSet = new HashSet<>();
hashSet.addAll(List.of("zebra", "apple", "banana"));
System.out.println(hashSet); // Could be: [banana, apple, zebra] (any order)

// LinkedHashSet - maintains insertion order
Set<String> linkedSet = new LinkedHashSet<>();
linkedSet.addAll(List.of("zebra", "apple", "banana"));
System.out.println(linkedSet); // [zebra, apple, banana] (insertion order)

// TreeSet - natural ordering (sorted)
Set<String> treeSet = new TreeSet<>();
treeSet.addAll(List.of("zebra", "apple", "banana"));
System.out.println(treeSet); // [apple, banana, zebra] (sorted)

// Set operations
Set<Integer> set1 = new HashSet<>(List.of(1, 2, 3, 4));
Set<Integer> set2 = new HashSet<>(List.of(3, 4, 5, 6));

// Union (all elements from both sets)
Set<Integer> union = new HashSet<>(set1);
union.addAll(set2);  // {1, 2, 3, 4, 5, 6}

// Intersection (common elements)
Set<Integer> intersection = new HashSet<>(set1);
intersection.retainAll(set2);  // {3, 4}

// Difference (elements in set1 but not set2)
Set<Integer> difference = new HashSet<>(set1);
difference.removeAll(set2);  // {1, 2}

💡 Learning Tip: Remember “HASH-LINKED-TREE” order: HashSet (no order), LinkedHashSet (insertion order), TreeSet (sorted order).

Q: Which Set implementation should you use if you need both fast lookups and predictable iteration order?
A: LinkedHashSet — provides O(1) operations like HashSet but maintains insertion order unlike HashSet.

🃏 Map Operations and Merge Method

Rule: Map provides various methods for conditional updates and bulk operations.

  • compute methods: Update based on key/value computation
  • merge(): Combine new value with existing value using a function
  • putIfAbsent(): Only put if key doesn’t exist
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 85);
scores.put("Bob", 92);

// merge() - combines values when key exists, inserts when key doesn't exist
scores.merge("Alice", 10, Integer::sum);    // 85 + 10 = 95 (key exists)
scores.merge("Charlie", 88, Integer::sum);  // Just inserts 88 (key doesn't exist)

System.out.println(scores); // {Alice=95, Bob=92, Charlie=88}

// computeIfAbsent - only compute if key missing
scores.computeIfAbsent("David", k -> k.length() * 10);  // David=50 (5 chars * 10)
scores.computeIfAbsent("Alice", k -> k.length() * 10);  // No change (Alice exists)

// computeIfPresent - only compute if key exists  
scores.computeIfPresent("Bob", (k, v) -> v + 5);        // Bob=97 (92 + 5)
scores.computeIfPresent("Eve", (k, v) -> v + 5);        // No change (Eve doesn't exist)

// compute - always computes (can return null to remove)
scores.compute("Alice", (k, v) -> v == null ? 100 : v - 10);  // Alice=85 (95 - 10)

Bulk operations:

Map<String, String> defaults = Map.of("theme", "dark", "lang", "en");
Map<String, String> userPrefs = new HashMap<>();
userPrefs.put("theme", "light");

// putAll vs merge behavior
userPrefs.putAll(defaults);  // Overwrites existing keys
// Result: {theme=dark, lang=en} - theme overwritten!

// Better: merge each entry
defaults.forEach((k, v) -> userPrefs.merge(k, v, (old, new_) -> old));
// Result: {theme=light, lang=en} - keeps existing theme

💡 Learning Tip: Think “MERGE = SMART PUT” - merge() handles both insertion and updating with custom logic.

Q: What happens when you call merge() with a key that doesn’t exist in the map?
A: The new value is simply inserted (put), and the merge function is not called since there’s no existing value to merge with.