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
| Need | Use |
|---|---|
| Ordered list, fast random access | ArrayList |
| Ordered list, fast insert/delete at ends | LinkedList |
| Unique elements, fast lookup, any order | HashSet |
| Unique elements, sorted | TreeSet |
| Unique elements, insertion order | LinkedHashSet |
| Key-value, fast lookup, any order | HashMap |
| Key-value, sorted by key | TreeMap |
| Key-value, insertion order | LinkedHashMap |
| FIFO queue or stack | ArrayDeque |
| Priority-based processing | PriorityQueue |
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);
}
}