Collections Framework
A comprehensive guide to the Java Collections Framework: the interface hierarchy, List, Set, Queue, Deque, and Map implementations, the Collections utility class, immutable factory methods, Comparable versus Comparator, iterators, and fail-fast versus fail-safe behaviour.
Collections Framework Overview
Before Java 2, the language had only arrays, the Vector class, and the Hashtable class for grouping objects. These were inconsistent, not always interchangeable, and lacked a common API. Java 2 shipped the Collections Framework to fix all of that: a unified architecture of interfaces, implementations, and algorithms for storing and manipulating groups of objects.
The framework gives you three things. First, a set of interfaces that define what a collection can do: add elements, remove them, iterate over them, check membership, and so on. Second, a set of concrete classes that implement those interfaces with different performance characteristics suited to different use cases. Third, a utility class called Collections that provides static algorithms such as sorting, shuffling, and searching.
Because everything programmes against the interface (not the implementation), you can swap one implementation for another with a single line change. Code written against List works the same whether you hand it an ArrayList or a LinkedList. This is the power of the framework.
Two separate hierarchies
The Collections Framework has two independent root interfaces. Understanding this split prevents a lot of confusion.
- Collection hierarchy:Rooted at Iterable, then Collection, then List, Set, and Queue. Everything here is a group of individual elements.
- Map hierarchy:Rooted at Map directly. Maps are not part of the Collection hierarchy because they store key-value pairs, not individual elements.
Collection Interface Hierarchy
At the very top sits java.lang.Iterable, which requires only one method: iterator(). Any class that implements Iterable can be used in an enhanced for-each loop. Below it sits java.util.Collection, which adds the core contract: size, isEmpty, contains, add, remove, and so on. From there the hierarchy branches into three main sub-interfaces.
The three main sub-interfaces of Collection
Each sub-interface adds semantics on top of the basic collection contract.
- List:An ordered sequence that allows duplicate elements. Elements are accessible by their integer index. Guarantees that insertion order is preserved.
- Set:A collection that contains no duplicate elements. Does not guarantee any particular iteration order (though some implementations do). Models the mathematical concept of a set.
- Queue:A collection designed for holding elements prior to processing. Typically (but not always) FIFO. Adds head-inspection and poll methods. Deque extends Queue to support both ends.
Iterable and Iterator Interface
Iterable<T> is the contract that says: "you can get a cursor to walk over my elements." It declares a single method, Iterator<T> iterator(), which returns a fresh cursor positioned before the first element.
The Iterator<T> interface itself has three methods. hasNext() returns true if there are more elements to visit. next() advances the cursor and returns the element it just passed. remove() deletes the element most recently returned by next() from the underlying collection. Using remove() through the iterator is the only safe way to delete elements while iterating. Calling the collection's own remove() during a for-each loop will trigger a ConcurrentModificationException.
The enhanced for-each loop is syntactic sugar over an iterator. When you write for (String s : list), the compiler rewrites it as a call to list.iterator() followed by a while loop using hasNext() and next().
Iterable and Iterator
JavaUsing an explicit iterator to walk a list and safely remove elements that match a condition.
List Interface: ArrayList, LinkedList, Vector, Stack
The List interface extends Collection with positional access methods: get(int index), set(int index, E element), add(int index, E element), and indexOf(Object). Lists preserve insertion order and allow duplicates.
ArrayList
The go-to List implementation for most use cases. Backed by a resizable array.
- Random access by index is O(1): retrieving element 500 from a 10,000-element list takes the same time as retrieving element 1, because the backing array is indexed directly.
- Adding to the end is amortised O(1): the array occasionally needs to be resized (typically doubled), but this cost is spread across many insertions.
- Insertions and deletions in the middle are O(n): every element after the insertion point must be shifted. This becomes expensive for large lists.
- Not thread-safe. For concurrent use, wrap with Collections.synchronizedList() or use CopyOnWriteArrayList from java.util.concurrent.
LinkedList
A doubly-linked list that also implements Deque. Each element is a node with references to the previous and next node.
- Insertions and deletions anywhere (once you have a reference to the position) are O(1): just re-point the neighbouring nodes.
- Random access by index is O(n): to reach element 500, the list must walk from the head (or tail) through 500 nodes.
- Uses more memory per element than ArrayList because each node stores two references (previous and next) in addition to the data.
- Best choice when you are doing frequent insertions and deletions at the front or middle, or when you need it to behave as a queue or deque.
Vector and Stack
Legacy classes from Java 1.0. Kept for backward compatibility; avoid them in new code.
- Vector is like ArrayList but every method is synchronized, which means it is thread-safe but also slower due to lock acquisition even in single-threaded scenarios.
- Stack extends Vector and adds push(), pop(), and peek() for LIFO behaviour. However, because it extends Vector, it also exposes all Vector methods, making it easy to violate stack semantics.
- For thread-safe lists in new code, prefer Collections.synchronizedList(new ArrayList<>()) or CopyOnWriteArrayList. For a stack, use ArrayDeque which is faster and better encapsulated.
ArrayList and LinkedList in Practice
JavaComparing positional access on ArrayList (fast) and LinkedList (slow), and LinkedList as a deque.
Set Interface: HashSet, LinkedHashSet, TreeSet
The Set interface guarantees that no duplicate elements can be stored. When you call add() and the element already exists, the set is unchanged and add() returns false. What counts as a duplicate is determined by the element's equals() and hashCode() methods (for hash-based sets) or its compareTo() method (for tree-based sets). Getting this contract wrong is the most common source of subtle bugs when using sets with custom objects.
HashSet, LinkedHashSet, and TreeSet compared
All three enforce no-duplicates. They differ in ordering and performance.
- HashSet:The fastest Set. O(1) average time for add, remove, and contains. No ordering guarantee: iteration order is unpredictable and may change as the set grows. The right default choice when order does not matter.
- LinkedHashSet:A HashSet with an additional linked list that records insertion order. O(1) average operations, slightly more memory than HashSet. Use it when you need fast lookups plus predictable insertion-order iteration.
- TreeSet:A Set backed by a red-black tree. O(log n) for add, remove, and contains. Iterates in natural sorted order (or the order defined by a Comparator). Adds navigation methods such as first(), last(), floor(), and ceiling().
HashSet, LinkedHashSet, and TreeSet
JavaThe same data added to all three sets to illustrate their different iteration orders.
Queue Interface: PriorityQueue, ArrayDeque, LinkedList as Queue
The Queue interface models a waiting line. Elements enter at the tail and leave from the head, which is the FIFO (first-in, first-out) pattern used in task scheduling, message passing, and breadth-first search. The interface provides two sets of methods for each operation: ones that throw exceptions on failure (add, remove, element) and ones that return null or false instead (offer, poll, peek). In most application code the null-returning versions are preferred because they avoid exception overhead in normal flow.
Queue implementations
Three commonly used Queue implementations, each suited to a different scenario.
- PriorityQueue:A min-heap. Elements are retrieved in natural order (smallest first) or by a supplied Comparator. Useful for implementing Dijkstra's algorithm, job scheduling by priority, or any situation where the next item to process is determined by importance rather than arrival time. O(log n) for offer and poll.
- ArrayDeque as Queue:A resizable circular array that works as a FIFO queue. Faster than LinkedList for most queue use cases because it avoids the overhead of node allocation. Prefer ArrayDeque whenever you need a plain FIFO queue without priority.
- LinkedList as Queue:LinkedList implements Queue and can serve as a FIFO queue. It is slightly slower than ArrayDeque for this purpose due to per-node allocation. Its main advantage is that it allows null elements, whereas ArrayDeque does not.
Queue and PriorityQueue
JavaA FIFO queue for processing tasks, and a PriorityQueue that retrieves the highest-priority task first.
Deque Interface: ArrayDeque and LinkedList as Deque
Deque (double-ended queue) extends Queue and allows insertion and removal at both ends. This makes it useful as a stack (LIFO) as well as a queue (FIFO), and for any algorithm that needs to work from both ends of a sequence, such as sliding-window problems or palindrome checking.
ArrayDeque is the recommended general-purpose deque. It is faster than LinkedList for both stack and queue operations because it uses a circular array with no per-element object allocation. It is also the preferred replacement for the legacy Stack class.
ArrayDeque as Stack and Deque
JavaUsing ArrayDeque to simulate a browser history (back/forward stack) and a sliding-window buffer.
Map Interface: HashMap, LinkedHashMap, TreeMap, Hashtable, ConcurrentHashMap
The Map interface is not part of the Collection hierarchy, but it is just as central to everyday Java programming. A Map stores key-value pairs where each key is unique. Given a key, retrieving its associated value is extremely efficient in hash-based implementations. Core operations are put(K key, V value), get(Object key), remove(Object key), containsKey(), keySet(), values(), and entrySet().
Map implementations compared
Choose based on whether you need ordering, thread safety, or raw performance.
- HashMap:The standard general-purpose map. O(1) average for get, put, and remove. No ordering of keys. Allows one null key and multiple null values. Not thread-safe.
- LinkedHashMap:Extends HashMap with a linked list that maintains insertion order (or optionally access order for LRU cache implementation). Slightly more memory. Same O(1) operations.
- TreeMap:A red-black tree map. Keys are stored in natural sorted order or by a Comparator. O(log n) operations. Adds navigation like floorKey(), ceilingKey(), headMap(), and tailMap().
- Hashtable:Legacy class from Java 1.0. Synchronised like Vector. Does not allow null keys or values. Prefer ConcurrentHashMap in new code that needs thread safety.
- ConcurrentHashMap:Thread-safe map from java.util.concurrent. Uses segment-level locking (Java 7) or node-level locking (Java 8+) for much better concurrent throughput than Hashtable. Does not allow null keys or values.
Map Operations and Implementations
JavaCore Map operations including getOrDefault, putIfAbsent, merge, and entrySet iteration.
Collections Utility Class
java.util.Collections (note the plural) is a class of static methods that operate on collections. It is distinct from the Collection interface itself. Think of it as a toolbox of algorithms that you can apply to any list, set, or other collection without being concerned about the underlying implementation.
The methods divide broadly into three groups: reordering methods that change the state of a list (sort, reverse, shuffle, rotate); searching and statistical methods that read from a collection without changing it (min, max, frequency, binarySearch, disjoint); and wrapping methods that return special-purpose views of collections (unmodifiableList, synchronizedList, singletonList, nCopies, emptyList).
Collections Utility Methods
JavaSorting, reversing, shuffling, finding min/max, checking frequency and disjoint, and creating unmodifiable views.
Arrays.asList() vs List.of(), Set.of(), and Map.of() (Java 9+)
Java provides several shortcuts for creating collections from a known set of values. Choosing the wrong one leads to subtle bugs, so understanding the differences is important.
Arrays.asList() returns a fixed-size list backed directly by the provided array. You can call set() to replace elements, but you cannot add or remove elements because the size is tied to the array. Changes to the original array are reflected in the list and vice versa.
Java 9 introduced List.of(), Set.of(), and Map.of() as truly immutable factory methods. None of these allow nulls, and any attempt to call add, remove, set, or put on them throws UnsupportedOperationException. They are compact, easy to read, and safe to share across threads without synchronisation. If you need a mutable starting point from an immutable literal, wrap it: new ArrayList<>(List.of(...)).
Arrays.asList() vs List.of() vs new ArrayList()
JavaShowing mutation behaviour differences between each creation method.
Comparable vs Comparator
Sorting requires some way to compare two objects and decide which comes first. Java provides two mechanisms for this: Comparable and Comparator. They solve the same problem but from different perspectives.
Comparable<T> is implemented by the class itself. Its single method, int compareTo(T other), defines the natural ordering of the class. Return a negative number if this object should come before other, zero if they are equal, and a positive number if this object should come after other. Classes like Integer, String, and LocalDate already implement Comparable.
Comparator<T> is an external strategy object. You create one when you want an ordering that is different from the natural ordering, or when you cannot modify the class (it belongs to a library). Since Java 8, Comparator is a functional interface, so you can express it as a lambda or use the convenient factory methods like Comparator.comparing(), thenComparing(), and reversed().
Comparable and Comparator
JavaA Product class with a natural ordering via Comparable, plus multiple external orderings via Comparator.
Iterator and ListIterator
You have already seen Iterator earlier in this tutorial, but it is worth comparing it directly with its more powerful sibling, ListIterator.
A ListIterator is obtained from any List by calling listIterator(). It extends Iterator and adds the ability to traverse backwards using hasPrevious() and previous(), to retrieve the current index using nextIndex() and previousIndex(), to replace the last element visited using set(), and to insert a new element at the current position using add(). This makes it the right tool when you need to transform a list in-place during a single pass.
Iterator vs ListIterator
JavaUsing ListIterator to traverse a list backwards and replace elements in-place.
Fail-fast vs Fail-safe Iterators
When you iterate over a collection and another piece of code modifies it concurrently (or even in the same thread, directly through the collection's own methods during a loop), the safety of the operation depends entirely on which type of iterator you are using.
Fail-fast iterators are used by the standard single-threaded collections: ArrayList, LinkedList, HashMap, HashSet, and their relatives. They maintain an internal modification count. Every structural change to the collection (adding, removing, or clearing elements) increments this count. Each time the iterator calls next() it checks whether the count has changed since the iterator was created. If it has, it immediately throws a ConcurrentModificationException. The name "fail-fast" is accurate: rather than producing silently wrong results, the iterator fails loudly as soon as it detects the problem.
Fail-safe iterators (found in the concurrent collections in java.util.concurrent, such as CopyOnWriteArrayList and ConcurrentHashMap) operate on a snapshot of the collection's state taken at the time the iterator was created. Modifications to the original collection after that point are not seen by the iterator, and no exception is thrown. The trade-off is memory: maintaining a snapshot is more expensive, and the iterator may see stale data.
Which iterator to use in which situation
The choice is largely determined by whether your code is single-threaded or multi-threaded.
- Single-threaded, no modification during iteration:The fail-fast iterator is perfect. Use a regular for-each loop.
- Single-threaded, need to remove during iteration:Use Iterator.remove() explicitly. This is the one structural modification that is permitted through the iterator itself.
- Multi-threaded, infrequent writes and frequent reads:CopyOnWriteArrayList provides a fail-safe iterator and is efficient when reads vastly outnumber writes.
- Multi-threaded, frequent updates:ConcurrentHashMap's iterator is weakly consistent: it reflects some but not necessarily all updates made after the iterator was created, without throwing an exception.
Fail-fast vs Fail-safe Iterator Behaviour
JavaTriggering ConcurrentModificationException with ArrayList, and avoiding it with CopyOnWriteArrayList.
Quiz - Test Your Knowledge
Ten questions covering the interface hierarchy, List and Set implementations, Queue and Map choices, the Collections utility class, immutable factory methods, Comparable versus Comparator, ListIterator capabilities, and fail-fast versus fail-safe behaviour. Read each option carefully before selecting your answer.
Knowledge Check
1. What is the root interface of the Java Collections Framework hierarchy (excluding Map)?
2. Which List implementation gives you the fastest random access by index but slow insertions and deletions in the middle?
3. What is the key difference between HashSet and LinkedHashSet?
4. A PriorityQueue in Java retrieves elements in which order by default?
5. Which Map implementation guarantees that keys are stored in sorted natural order?
6. What is the main practical difference between Comparable and Comparator?
7. What does a fail-fast iterator do when the underlying collection is structurally modified during iteration?
8. What is the key difference between Arrays.asList() and List.of() (Java 9+)?
9. Which additional capability does a ListIterator have that a regular Iterator does not?
10. Which of the following correctly describes Collections.unmodifiableList()?