Skip to main content
Java intermediate Lesson 29 of 58

Collections Framework in Java

Master Java's Collections Framework — ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap, Queue, and PriorityQueue with practical examples.

The Java Collections Framework provides ready-made data structures for storing and manipulating groups of objects. Rather than implementing linked lists, hash tables, or priority queues from scratch, you get battle-tested, generic implementations that work with any type. Every collection implements one of the core interfaces: List, Set, Map, or Queue, so you can swap implementations by changing one line.

Collection Interfaces Overview

Understanding the interface hierarchy helps you pick the right tool. Each interface makes different guarantees about ordering, uniqueness, and access patterns.

Collection
├── List      — ordered, allows duplicates (ArrayList, LinkedList)
├── Set       — no duplicates (HashSet, TreeSet, LinkedHashSet)
└── Queue     — FIFO processing (ArrayDeque, PriorityQueue, LinkedList)

Map           — key→value pairs, keys are unique (HashMap, TreeMap, LinkedHashMap)

List

ArrayList — Resizable Array

ArrayList is the go-to general-purpose list. It stores elements in a contiguous array that grows automatically when capacity is exceeded, giving O(1) random access and fast iteration. It is the right default for any ordered, indexable sequence.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

List<String> names = new ArrayList<>();

// Adding elements
names.add("Alice");
names.add("Bob");
names.add("Charlie");
names.add(1, "Dave");    // insert at index 1 — shifts others right

// Accessing
System.out.println(names.get(0));          // "Alice"
System.out.println(names.size());          // 4
System.out.println(names.contains("Bob")); // true
System.out.println(names.indexOf("Bob"));  // 2

// Updating and removing
names.set(0, "Alicia");              // replace at index 0
names.remove("Dave");                // remove by value
names.remove(0);                     // remove by index

// Iteration
for (String name : names) System.out.println(name);
names.forEach(System.out::println);  // Java 8+ style

// Sorting
Collections.sort(names);                                        // alphabetical
Collections.sort(names, Collections.reverseOrder());            // reverse
names.sort((a, b) -> a.length() - b.length());                 // by length

// Useful utility operations
Collections.shuffle(names);
Collections.reverse(names);
String min = Collections.min(names);
String max = Collections.max(names);

// Create from existing values
List<Integer> nums = new ArrayList<>(List.of(5, 2, 8, 1, 9));

LinkedList — Doubly-Linked List

LinkedList stores elements as nodes with pointers, making prepend and remove-from-front O(1) instead of O(n). It also implements Deque, so it doubles as both a stack and a queue without any extra overhead.

import java.util.LinkedList;

LinkedList<String> list = new LinkedList<>();
list.add("B");
list.addFirst("A");     // prepend — O(1), no shifting
list.addLast("C");      // append — O(1)

System.out.println(list.getFirst()); // "A"
System.out.println(list.getLast());  // "C"

list.removeFirst();
list.removeLast();
System.out.println(list); // [B]

// Used as a stack (LIFO) — push/pop operate on the front
list.push("X");    // addFirst
list.push("Y");
System.out.println(list.pop()); // "Y" — removeFirst (last in, first out)

Set

HashSet — No Duplicates, No Order

HashSet uses a hash table internally, giving O(1) average-case add, remove, and contains. It is the right choice whenever you need uniqueness guarantees and don’t care about the order of elements. It also supports the classic set operations — union, intersection, and difference.

import java.util.HashSet;
import java.util.Set;

Set<String> set = new HashSet<>();
set.add("apple");
set.add("banana");
set.add("apple");   // duplicate — silently ignored, size stays 2
System.out.println(set.size()); // 2

System.out.println(set.contains("banana")); // true
set.remove("banana");

// Set operations — create copies to avoid modifying the originals
Set<Integer> a = new HashSet<>(Set.of(1, 2, 3, 4));
Set<Integer> b = new HashSet<>(Set.of(3, 4, 5, 6));

Set<Integer> union = new HashSet<>(a);
union.addAll(b);             // {1, 2, 3, 4, 5, 6}

Set<Integer> intersection = new HashSet<>(a);
intersection.retainAll(b);   // {3, 4}

Set<Integer> difference = new HashSet<>(a);
difference.removeAll(b);     // {1, 2}

TreeSet — Sorted Set

TreeSet stores elements in a red-black tree, keeping them in natural sorted order at all times. The cost is O(log n) for add/remove/contains instead of O(1), but you gain powerful range-query methods — floor, ceiling, headSet, tailSet — that HashSet simply cannot offer.

import java.util.TreeSet;

TreeSet<Integer> sorted = new TreeSet<>(Set.of(5, 2, 8, 1, 9, 3));
System.out.println(sorted);           // [1, 2, 3, 5, 8, 9] — always sorted
System.out.println(sorted.first());   // 1
System.out.println(sorted.last());    // 9
System.out.println(sorted.floor(4));    // 3 — largest element ≤ 4
System.out.println(sorted.ceiling(4)); // 5 — smallest element ≥ 4
System.out.println(sorted.headSet(5)); // [1, 2, 3] — elements < 5
System.out.println(sorted.tailSet(5)); // [5, 8, 9] — elements ≥ 5

LinkedHashSet — Insertion-Order Set

LinkedHashSet adds a linked list on top of the hash table, preserving the order in which elements were inserted. It gives you uniqueness guarantees without sacrificing predictable iteration order — useful when output order matters for display or reproducibility.

import java.util.LinkedHashSet;
Set<String> ordered = new LinkedHashSet<>();
ordered.add("banana");
ordered.add("apple");
ordered.add("cherry");
System.out.println(ordered); // [banana, apple, cherry] — insertion order preserved

Map

HashMap — Key → Value, No Ordering

HashMap is the standard key-value store. It uses hashing to achieve O(1) average get and put, making it the right default when you need to look up values by a key. The merge, computeIfAbsent, and computeIfPresent methods introduced in Java 8 are especially handy for patterns like grouping and counting.

import java.util.HashMap;
import java.util.Map;

Map<String, Integer> scores = new HashMap<>();

// Adding and updating
scores.put("Alice", 95);
scores.put("Bob", 87);
scores.put("Charlie", 92);
scores.put("Alice", 98);   // replaces the existing value for "Alice"

// Reading
System.out.println(scores.get("Alice"));             // 98
System.out.println(scores.getOrDefault("Dave", 0));  // 0 — safe default for missing keys
System.out.println(scores.containsKey("Bob"));       // true
System.out.println(scores.containsValue(92));        // true
System.out.println(scores.size());                   // 3

// Removing
scores.remove("Bob");
scores.remove("Charlie", 50); // conditional remove — no-op here since value is 92

// Iterating
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}
scores.forEach((name, score) -> System.out.println(name + ": " + score));

// Key and value views
Set<String>         keys   = scores.keySet();
Collection<Integer> values = scores.values();

// Atomic update operations
scores.merge("Alice", 5, Integer::sum);          // Alice: 98+5=103
scores.computeIfAbsent("Eve", k -> 100);         // adds Eve=100 only if not present
scores.computeIfPresent("Eve", (k, v) -> v + 10); // Eve: 110

TreeMap — Sorted by Key

TreeMap keeps entries sorted by key in natural order (or a provided Comparator). Like TreeSet, it offers range-query methods on keys. Use it when you need to iterate entries in key order or perform sub-map queries.

import java.util.TreeMap;

TreeMap<String, Integer> sorted = new TreeMap<>();
sorted.put("Charlie", 92);
sorted.put("Alice", 95);
sorted.put("Bob", 87);
System.out.println(sorted); // {Alice=95, Bob=87, Charlie=92} — sorted by key

System.out.println(sorted.firstKey());   // "Alice"
System.out.println(sorted.lastKey());    // "Charlie"
System.out.println(sorted.headMap("C")); // {Alice=95, Bob=87} — keys before "C"

LinkedHashMap — Insertion Order Preserved

LinkedHashMap preserves insertion order during iteration, unlike HashMap. It is the right choice when you need a map whose iteration order is stable and matches the order you put things in — for example, building an ordered configuration map or maintaining a display order.

import java.util.LinkedHashMap;
Map<String, Integer> lru = new LinkedHashMap<>();
lru.put("first", 1);
lru.put("second", 2);
lru.put("third", 3);
System.out.println(lru); // {first=1, second=2, third=3} — insertion order guaranteed

Queue and Deque

ArrayDeque — Efficient Double-Ended Queue

ArrayDeque is a resizable array-backed deque that is faster than LinkedList for both stack and queue use cases. Use it as your default Queue or Stack implementation — it avoids the overhead of node allocation that LinkedList incurs.

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Queue;

// Used as a Queue (FIFO) — offer adds to tail, poll removes from head
Queue<String> queue = new ArrayDeque<>();
queue.offer("first");
queue.offer("second");
queue.offer("third");
System.out.println(queue.peek());   // "first" — view without removing
System.out.println(queue.poll());   // "first" — remove and return
System.out.println(queue.poll());   // "second"

// Used as a Stack (LIFO) — push/pop both operate on the head
Deque<String> stack = new ArrayDeque<>();
stack.push("bottom");
stack.push("middle");
stack.push("top");
System.out.println(stack.pop());    // "top"
System.out.println(stack.pop());    // "middle"

PriorityQueue — Min-Heap by Default

PriorityQueue processes elements in priority order rather than insertion order. By default it is a min-heap — poll() always returns the smallest element. Supply a Comparator to change the priority. This is ideal for scheduling, pathfinding, or any problem where you repeatedly need “the best remaining item.”

import java.util.PriorityQueue;

// Min-heap — smallest element is always at the head
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(5);
minHeap.offer(1);
minHeap.offer(3);
minHeap.offer(2);

while (!minHeap.isEmpty()) {
    System.out.print(minHeap.poll() + " ");
}
// 1 2 3 5 — always removes the smallest regardless of insertion order

// Max-heap — reverse the natural ordering
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
maxHeap.offer(5); maxHeap.offer(1); maxHeap.offer(3);
System.out.println(maxHeap.poll()); // 5

// Custom priority — sort strings by length, shortest first
PriorityQueue<String> byLength = new PriorityQueue<>(Comparator.comparingInt(String::length));
byLength.offer("banana"); byLength.offer("kiwi"); byLength.offer("fig");
System.out.println(byLength.poll()); // "fig" (shortest)

Choosing the Right Collection

NeedUse
Ordered list, fast random accessArrayList
Ordered list, fast insert/delete at endsLinkedList
Unique elements, fast lookup, any orderHashSet
Unique elements, sortedTreeSet
Unique elements, insertion orderLinkedHashSet
Key-value, fast lookup, any orderHashMap
Key-value, sorted by keyTreeMap
Key-value, insertion orderLinkedHashMap
FIFO queue or stackArrayDeque
Priority-based processingPriorityQueue

Project: Library Management System

This project combines multiple collections to solve a real problem: a HashMap for O(1) book lookup by ISBN, a HashSet for O(1) checked-out status checks, and a HashMap<String, List<String>> for per-book waitlists. Choosing the right collection for each role makes the logic simple and efficient.

import java.util.*;

public class Library {

    record Book(String isbn, String title, String author, int year) {
        @Override public String toString() {
            return String.format("[%s] %s by %s (%d)", isbn, title, author, year);
        }
    }

    // HashMap for O(1) lookup by ISBN
    private final Map<String, Book>          catalog    = new HashMap<>();
    // HashSet for O(1) checked-out status check
    private final Set<String>                checkedOut = new HashSet<>();
    // HashMap of lists to maintain per-book waitlists
    private final Map<String, List<String>>  waitlists  = new HashMap<>();

    public void addBook(Book book) {
        catalog.put(book.isbn(), book);
    }

    public boolean checkout(String isbn, String patron) {
        if (!catalog.containsKey(isbn)) { System.out.println("Book not found"); return false; }
        if (checkedOut.contains(isbn)) {
            // computeIfAbsent creates the list on first use
            waitlists.computeIfAbsent(isbn, k -> new ArrayList<>()).add(patron);
            System.out.println(patron + " added to waitlist for " + catalog.get(isbn).title());
            return false;
        }
        checkedOut.add(isbn);
        System.out.println(patron + " checked out: " + catalog.get(isbn).title());
        return true;
    }

    public void returnBook(String isbn) {
        if (!checkedOut.remove(isbn)) { System.out.println("Was not checked out"); return; }
        Book book = catalog.get(isbn);
        List<String> waitlist = waitlists.getOrDefault(isbn, List.of());
        if (!waitlist.isEmpty()) {
            String nextPatron = waitlist.remove(0); // FIFO: first person on waitlist gets it
            checkout(isbn, nextPatron);
        } else {
            System.out.println(book.title() + " returned to shelf");
        }
    }

    public List<Book> searchByAuthor(String author) {
        return catalog.values().stream()
            .filter(b -> b.author().equalsIgnoreCase(author))
            .sorted(Comparator.comparing(Book::year))
            .toList();
    }

    public static void main(String[] args) {
        Library lib = new Library();
        lib.addBook(new Book("978-0", "Clean Code", "Robert Martin", 2008));
        lib.addBook(new Book("978-1", "Effective Java", "Joshua Bloch", 2018));
        lib.addBook(new Book("978-2", "Refactoring", "Martin Fowler", 1999));

        lib.checkout("978-1", "Alice");
        lib.checkout("978-1", "Bob");   // goes on waitlist
        lib.returnBook("978-1");        // Bob auto-checked-out from waitlist
        lib.returnBook("978-1");        // back to shelf

        lib.searchByAuthor("Martin Fowler").forEach(System.out::println);
    }
}

Frequently Asked Questions

When should I use ArrayList vs LinkedList?
Use ArrayList (default) for most cases — random access by index is O(1). Use LinkedList when you frequently insert/delete at the beginning or middle of the list — those operations are O(1) vs O(n) for ArrayList. LinkedList also implements Deque so it works as a stack or queue.
What is the difference between HashMap and TreeMap?
HashMap stores entries in no particular order and provides O(1) average get/put. TreeMap keeps entries sorted by key and provides O(log n) operations. Use HashMap when order doesn't matter; use TreeMap when you need sorted keys or range queries.
Why can't I put primitives in a List?
Java generics require object types. Use the wrapper classes: List<Integer> not List<int>. Java autoboxes int to Integer automatically when you add to the list, but be aware this has a small overhead.