V
Vel·ToolKit
Simple · Fast · Ready to use
EN
Chapter 13 of 20

Collections Framework

List / Set / Map / Queue and mainstream implementations, choosing, iteration pitfalls

The Collections Framework at a Glance

The Java Collections Framework (java.util) consists of a few core interfaces plus their implementations. Understanding the interface semantics and implementation trade-offs is the foundation of writing Java efficiently.

Collection (top)
├── List       ordered, allows duplicates, index access
│   ├── ArrayList    array-backed, fast random access (use it 90% of the time)
│   └── LinkedList   linked-list, fast head/tail insertion
├── Set        no duplicates, generally unordered
│   ├── HashSet      hash table, O(1) lookup, the most common
│   ├── LinkedHashSet keeps insertion order
│   └── TreeSet      red-black tree, auto-sorted
└── Queue      queue: FIFO / priority queue / deque
    ├── ArrayDeque    deque, great as both stack and queue
    ├── LinkedList    also implements Queue
    └── PriorityQueue binary heap, dequeues by priority

Map (separate hierarchy)
├── HashMap         hash table, O(1), the most common
├── LinkedHashMap   keeps insertion order (or access order, for LRU)
├── TreeMap         red-black tree, sorted by key
└── ConcurrentHashMap concurrency-safe, essential for multithreading

List: ArrayList Is the Workhorse

Use case: 99% of "I need a dynamic array" — order lists, query results, config items. LinkedList only wins in the rare case of frequent head/tail insertion without random access; rarely used day to day.

// ListDemo.java
import java.util.ArrayList;
import java.util.List;

public class ListDemo {
    public static void main(String[] args) {
        List<String> users = new ArrayList<>();
        users.add("Alice");
        users.add("Bob");
        users.add(1, "Carol");           // insert at index 1
        System.out.println(users);        // [Alice, Carol, Bob]

        System.out.println(users.get(0));      // Alice
        System.out.println(users.size());      // 3
        System.out.println(users.contains("Bob")); // true
        System.out.println(users.indexOf("Bob"));  // 2

        users.remove("Carol");
        users.set(0, "ALICE");
        System.out.println(users);

        // sub-list (a view; modifying it affects the original)
        List<String> sub = users.subList(0, 1);
        System.out.println(sub);
    }
}

Set: the Deduplication Trio

Use cases for the three Sets: HashSet (most common, you just need "dedup"); LinkedHashSet (dedup and keep insertion order, for a "unique list"); TreeSet (dedup and auto-sort, for a "leaderboard").

// SetDemo.java
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;

public class SetDemo {
    public static void main(String[] args) {
        Set<String> tags = new HashSet<>();
        tags.add("java");
        tags.add("go");
        tags.add("java");             // duplicate, not added again
        System.out.println(tags);       // order not guaranteed

        Set<String> ordered = new LinkedHashSet<>();
        ordered.add("b"); ordered.add("a"); ordered.add("c");
        System.out.println(ordered);    // [b, a, c]

        Set<Integer> sorted = new TreeSet<>();
        sorted.add(3); sorted.add(1); sorted.add(2);
        System.out.println(sorted);     // [1, 2, 3]
    }
}

Map: Key-Value Storage

Use cases: look up a user by ID, geolocation by IP, cache a computed result by key, count word frequencies. HashMap is the workhorse; LinkedHashMap for an LRU cache; TreeMap for range queries (subMap / floorKey); ConcurrentHashMap for sharing across threads.

// MapDemo.java
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

public class MapDemo {
    public static void main(String[] args) {
        Map<String, Integer> stock = new HashMap<>();
        stock.put("apple", 5);
        stock.put("pear", 3);

        System.out.println(stock.get("apple"));               // 5
        System.out.println(stock.getOrDefault("melon", 0));    // 0
        System.out.println(stock.containsKey("apple"));        // true

        // put only if absent (avoid overwriting)
        stock.putIfAbsent("apple", 999);
        System.out.println(stock.get("apple"));                // still 5

        // the classic word-frequency pattern
        Map<String, Integer> freq = new HashMap<>();
        for (String w : new String[]{"a", "b", "a", "c", "b", "a"}) {
            freq.merge(w, 1, Integer::sum);
        }
        System.out.println(freq); // {a=3, b=2, c=1}

        // iterate
        for (Map.Entry<String, Integer> e : stock.entrySet()) {
            System.out.println(e.getKey() + "=" + e.getValue());
        }

        // TreeMap range query
        TreeMap<Integer, String> grades = new TreeMap<>();
        grades.put(60, "D"); grades.put(70, "C"); grades.put(80, "B"); grades.put(90, "A");
        System.out.println(grades.floorEntry(75));  // largest key <=75: 70=C
        System.out.println(grades.headMap(80));     // the submap of keys < 80
    }
}

Queue and Deque

Use cases: breadth-first search (BFS), task queuing, an undo stack, message buffering. ArrayDeque works as both a queue (FIFO) and a stack (LIFO), with better performance than the Stack class — the workhorse. PriorityQueue dequeues by priority, for "todos sorted by due date" or Dijkstra.

// QueueDemo.java
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.PriorityQueue;

public class QueueDemo {
    public static void main(String[] args) {
        // FIFO queue
        Deque<String> q = new ArrayDeque<>();
        q.offer("a"); q.offer("b"); q.offer("c");
        while (!q.isEmpty()) {
            System.out.println(q.poll()); // a b c
        }

        // stack usage (push/pop)
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(1); stack.push(2); stack.push(3);
        while (!stack.isEmpty()) {
            System.out.println(stack.pop()); // 3 2 1
        }

        // priority queue: min-heap by default
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        pq.offer(5); pq.offer(1); pq.offer(3);
        while (!pq.isEmpty()) {
            System.out.println(pq.poll()); // 1 3 5
        }
    }
}

Immutable Collections List.of / Map.of (Java 9+)

Use cases: config constants, passing a read-only view, preventing downstream mutation. List.of(), Set.of(), Map.of() return truly immutable collections (add/remove throw); elements cannot be null.

// Immutable.java
import java.util.List;
import java.util.Map;
import java.util.Set;

public class Immutable {
    static final List<String> PROFILES = List.of("dev", "staging", "prod");
    static final Map<String, Integer> LIMITS = Map.of(
        "free", 100,
        "pro",  10_000,
        "max",  Integer.MAX_VALUE
    );

    public static void main(String[] args) {
        System.out.println(PROFILES);
        System.out.println(LIMITS);

        // PROFILES.add("x"); // throws UnsupportedOperationException

        // to make an immutable copy from a mutable collection:
        List<Integer> source = new java.util.ArrayList<>(List.of(1, 2, 3));
        List<Integer> readonly = List.copyOf(source);
        System.out.println(readonly);
    }
}

Iteration and the ConcurrentModificationException Pitfall

Calling the collection's add/remove during a for-each iteration throws ConcurrentModificationException. To remove while iterating, use the iterator's own remove() method, or use removeIf().

// IterationTrap.java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class IterationTrap {
    public static void main(String[] args) {
        List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4, 5));

        // ❌ throws ConcurrentModificationException
        // for (int n : nums) {
        //     if (n % 2 == 0) nums.remove(Integer.valueOf(n));
        // }

        // ✅ use Iterator.remove()
        Iterator<Integer> it = nums.iterator();
        while (it.hasNext()) {
            if (it.next() % 2 == 0) it.remove();
        }
        System.out.println(nums); // [1, 3, 5]

        // ✅ shorter: removeIf (Java 8+)
        List<Integer> nums2 = new ArrayList<>(List.of(1, 2, 3, 4, 5));
        nums2.removeIf(n -> n % 2 == 0);
        System.out.println(nums2); // [1, 3, 5]
    }
}

The Collections Utility Class

Collections (note the "s") is a utility class providing static methods for sorting, searching, max/min, shuffle, reverse, immutable wrappers, etc. After Java 8 some of this has handier forms on List.sort / stream.

// CollectionsUtil.java
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class CollectionsUtil {
    public static void main(String[] args) {
        List<Integer> nums = new ArrayList<>(List.of(3, 1, 4, 1, 5, 9, 2, 6));

        Collections.sort(nums);
        System.out.println(nums);                    // ascending
        Collections.sort(nums, Comparator.reverseOrder());
        System.out.println(nums);                    // descending

        System.out.println(Collections.max(nums));
        System.out.println(Collections.min(nums));
        Collections.shuffle(nums);
        Collections.reverse(nums);
        System.out.println(nums);

        List<String> readonly = Collections.unmodifiableList(new ArrayList<>(List.of("a", "b")));
        // readonly.add("c"); // UnsupportedOperationException
    }
}