Key Takeaways
- Choose the interface by behavior: List for positions, Set for uniqueness, Queue or Deque for processing, and Map for key-based lookup.
- Start with ArrayList, HashSet, HashMap, or ArrayDeque for ordinary mutable collections. Choose linked or sorted implementations when their ordering guarantees matter.
- Encounter order, sorted order, null handling, mutability, and thread safety are separate properties. Check each before choosing an implementation.
- Java 21 introduced sequenced interfaces for consistent first, last, and reverse-view operations. A view shares backing storage; it isn't an independent copy.
The Java Collections Framework provides interfaces, implementations, and algorithms for grouping and processing objects. The right collection depends on the operations you need and the behavior you must preserve—not simply which class has the fastest-looking Big-O notation.
A collection groups elements into a single unit so you can iterate, search, transform, and aggregate them. This tutorial follows the interfaces first, then explains which implementations fit each job.
What Are Collections in Java?
Here's a list containing two strings. Save this example as Main.java and run its main method using JDK 21 or later. Later Java snippets are independent method-body fragments: replace this main method's contents with one fragment at a time, keeping the import and class.
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> words = new ArrayList<>();
words.add("hello");
words.add("world");
for (String word : words) {
System.out.println(word);
}
}
}hello
worldList is the interface—the contract your code uses. ArrayList is the implementation providing that behavior. Declaring variables using an interface keeps your code focused on its requirements rather than unnecessary implementation details.
The Java Collections Framework Hierarchy
Collection extends Iterable, which enables enhanced for-loops. Map belongs to the framework but is a separate hierarchy: it isn't a subtype of Collection. The diagram below shows the main relationships, including the sequenced interfaces.
Iterable
`-- Collection
|-- Set
|-- Queue
`-- SequencedCollection
|-- List
|-- Deque (also extends Queue)
`-- SequencedSet (also extends Set)
`-- SortedSet
`-- NavigableSet
Map (separate hierarchy)
`-- SequencedMap
`-- SortedMap
`-- NavigableMap
Selected interfaces only; concurrent branches omitted.
The official collections learning guide provides a broader tour. Don't confuse Collection, the interface for groups of elements, with Collections, the utility class containing static algorithms and wrappers.
Collection interfaces and classes are generic: List<String> constrains the element type to String. Copying strings into another collection doesn't convert them into integers. See Java generics and type inference if those type arguments are unfamiliar.
The Collection Interface: Shared Operations
The Collection contract includes size(), isEmpty(), contains(), iterator(), and toArray(). Modification operations include add(), remove(), clear(), and bulk operations such as addAll(), removeAll(), and retainAll().
// Fragment for Main.main
Collection<String> originals = new HashSet<>();
originals.add("hello");
originals.add("goodbye");
List<String> copy = new ArrayList<>(originals);
System.out.println(copy.size()); // 2
System.out.println(copy.isEmpty()); // false
System.out.println(copy.contains("hello")); // true
System.out.println(copy.contains("world")); // false
copy.add("another string");
copy.removeAll(originals);
System.out.println(copy); // [another string]The constructor belongs to ArrayList, not Collection. It creates independent collection storage containing the same element references: a shallow copy, not a deep copy of the objects.
Modification methods are optional operations. An implementation that doesn't support one can throw UnsupportedOperationException; an interface type alone doesn't promise mutability. retainAll() keeps elements present in another collection, while removeAll() removes matching elements from the receiving collection.
Iteration: Loops, Iterators, and Streams
Enhanced For-Loops for Straightforward Traversal
Use the enhanced for-loop from the first example for simple side effects or control flow. It doesn't expose a removal operation; calling collection.remove() during traversal of an ordinary fail-fast collection can cause ConcurrentModificationException.
Iterator for Controlled Removal
An Iterator exposes hasNext(), next(), and an optional remove() operation. remove() removes the last element returned by next(), and can be called only once per next() call.
// Fragment for Main.main
List<String> words = new ArrayList<>(List.of("hello", "goodbye"));
for (Iterator<String> it = words.iterator(); it.hasNext();) {
if (!"hello".equals(it.next())) {
it.remove();
}
}
System.out.println(words); // [hello]For objects, == compares references, not their logical values. Use equals() for String content comparisons. For simple in-place filtering, words.removeIf(word -> !"hello".equals(word)) is another option.
Streams for Transformations and Aggregation
A stream expresses a pipeline rather than replacing every loop. Use it when filtering, mapping, or aggregating makes the operation clearer; don't modify its source while the pipeline executes.
// Fragment for Main.main
List<String> words = new ArrayList<>(List.of("hello", "goodbye"));
List<String> greetings = words.stream()
.filter(word -> !"goodbye".equals(word))
.toList();
System.out.println(greetings); // [hello]
System.out.println(words); // [hello, goodbye]filter() selects elements without removing them from the source. Stream.toList() returns an unmodifiable list. For the function syntax used here, see Java lambda expressions.
The List Interface
A List has positional encounter order—the order in which traversal visits its elements—and permits duplicates. Ordered doesn't mean sorted: a list can contain [3, 1, 6].
Positional Access and Search
// Fragment for Main.main
List<String> words = new ArrayList<>();
words.add("hello");
words.add("goodbye");
words.add(1, "how are you?");
words.remove(0);
System.out.println(words); // [how are you?, goodbye]
List<String> repeated = new ArrayList<>(
List.of("hello", "how are you", "goodbye", "hello"));
System.out.println(repeated.indexOf("hello")); // 0
System.out.println(repeated.lastIndexOf("hello")); // 3get(index) reads an element; set(index, value) replaces it. add(index, value) inserts and shifts subsequent positions, while addAll(index, collection) inserts a group. indexOf() and lastIndexOf() return the first and last matching indexes, or -1 when absent.
Bidirectional Traversal with ListIterator
A ListIterator traverses in both directions and supports optional add(), set(), and remove() operations. Its cursor sits between elements; nextIndex() and previousIndex() describe the adjacent positions.
// Fragment for Main.main
List<Character> letters = new ArrayList<>(List.of('a', 'b', 'c'));
ListIterator<Character> it = letters.listIterator();
while (it.hasNext()) {
System.out.println(it.next());
}
while (it.hasPrevious()) {
System.out.println(it.previous());
it.set('a');
}
for (char letter : letters) {
System.out.println(letter);
}This prints a, b, c while moving forward, c, b, a while moving backward, then a, a, a after replacement. set() replaces the element most recently returned by next() or previous().
Range Views with subList()
// Fragment for Main.main
List<Character> letters = new ArrayList<>(List.of('a', 'b', 'c'));
letters.subList(0, 1).clear();
System.out.println(letters); // [b, c]
List<Character> independent = new ArrayList<>(letters.subList(0, 1));
System.out.println(independent); // [b]subList(fromIndex, toIndex) includes the start index and excludes the end index. It returns a backed view, so clearing the range removes elements from the original list. Construct a new ArrayList from the range when you need independent storage.
List Implementations
ArrayList: The Default Choice
ArrayList uses a resizable array. It provides constant-time indexed reads and replacements, amortized constant-time append, and linear-time insertion or removal in the middle. Amortized means occasional resizing is spread across many additions.
LinkedList: A Specialized Alternative
LinkedList is a doubly linked list implementing both List and Deque. End operations are constant time, but indexed access requires traversal. Insertion or removal through an already-positioned iterator avoids another search; locating that position still has a cost.
Its per-element nodes add allocation overhead and pointer chasing. Don't choose it simply because your code removes elements; the official ArrayList versus LinkedList comparison explains why hardware behavior matters alongside complexity.
Vector: Legacy, Not a Concurrency Strategy
Vector is a legacy growable-array class with synchronized methods. That doesn't make a sequence such as checking size and then reading an index atomic, or make iteration automatically safe against concurrent changes. Prefer ArrayList for ordinary use; choose a deliberate concurrency policy for shared access.
The Set Interface
A Set contains no duplicate elements. Hash-based sets use equals() and hashCode(); sorted sets use ordering comparisons to determine whether elements occupy the same position.
Detecting Duplicates with Set.add()
Set.add() returns false when the element is already present. That makes duplicate detection concise:
// Fragment for Main.main
List<Character> input = List.of('a', 'b', 'b', 'c', 'd', 'd');
Set<Character> distinct = new LinkedHashSet<>();
Set<Character> duplicates = new LinkedHashSet<>();
for (char item : input) {
if (!distinct.add(item)) {
duplicates.add(item);
}
}
System.out.println(distinct); // [a, b, c, d]
distinct.removeAll(duplicates);
System.out.println(distinct); // [a, c]
System.out.println(duplicates); // [b, d]Before removeAll(), distinct contains every different value, including values that occurred multiple times. Afterward, it contains only values occurring once. LinkedHashSet makes the displayed order deterministic; HashSet wouldn't guarantee that order.
HashSet, TreeSet, or LinkedHashSet?
- HashSet: Choose it for uniqueness and membership checks when traversal order doesn't matter.
- LinkedHashSet: Choose it for uniqueness with insertion encounter order. Ordinary add() of an existing element doesn't move it.
- TreeSet: Choose it to maintain sorted order or query ranges. It uses natural ordering unless you supply a Comparator.
HashSet and LinkedHashSet offer expected constant-time add(), remove(), and contains() under suitable hash distribution. TreeSet guarantees logarithmic time for those operations. Insertion order and sorted order solve different problems; neither makes one implementation universally better.
The Queue Interface
A Queue holds elements for processing. First-in-first-out, or FIFO, is common but not guaranteed by the interface: PriorityQueue selects by priority instead.
| Operation | Exception form | Sentinel form |
|---|---|---|
| Insert | add(e): throws IllegalStateException when capacity prevents insertion | offer(e): returns false when capacity prevents insertion |
| Remove head | remove(): throws NoSuchElementException when empty | poll(): returns null when empty |
| Inspect head | element(): throws NoSuchElementException when empty | peek(): returns null when empty |
The sentinel insertion form matters especially for bounded queues. An unbounded queue doesn't normally reject insertion because it is full, though resource failures remain possible. offer() can still throw for an invalid element; it isn't a universal no-exceptions API.
// Fragment for Main.main
Queue<Character> queue = new ArrayDeque<>(List.of('a', 'b', 'c', 'd'));
queue.add('e');
queue.offer('f');
System.out.println(queue.remove()); // a
System.out.println(queue.poll()); // b
System.out.println(queue.element()); // c
System.out.println(queue.peek()); // c
System.out.println(queue); // [c, d, e, f]Queue Implementations
Use ArrayDeque for a typical non-concurrent FIFO queue. LinkedList also implements Queue, but its additional List behavior isn't usually needed for queue processing.
PriorityQueue is heap-based. Its head is the least element under natural ordering or a supplied Comparator; ties aren't FIFO. Repeated removal follows that ordering, but iteration isn't sorted. It rejects null, provides logarithmic insertion and head removal, and constant-time head inspection.
These ordinary implementations aren't thread-safe. Concurrent and blocking queue alternatives are covered below; thread safety is an implementation property, not a prohibition imposed by Queue.
The Deque Interface
A Deque is a double-ended queue supporting insertion, inspection, and removal at either end. It extends Queue and SequencedCollection; it doesn't implement Stack, which is a legacy class.
// Fragment for Main.main
Deque<Character> deque = new ArrayDeque<>(List.of('a', 'b', 'c', 'd'));
deque.addFirst('e');
deque.offerLast('f');
deque.removeFirst(); // removes e
deque.pollLast(); // removes f
System.out.println(deque.getFirst()); // a
System.out.println(deque.peekLast()); // d
deque.removeFirstOccurrence('b');
System.out.println(deque); // [a, c, d]
Deque<String> stack = new ArrayDeque<>();
stack.push("first");
stack.push("second");
System.out.println(stack.peek()); // second
System.out.println(stack.pop()); // secondThe first/last methods come in the same exception-versus-sentinel pairs as Queue: for example, removeLast() versus pollLast(). removeFirstOccurrence() and removeLastOccurrence() remove a matching interior element and return false if none exists.
For a last-in-first-out, or LIFO, stack, use push(), pop(), and peek() on a Deque instead of the legacy Stack class.
ArrayDeque Versus LinkedList
Prefer ArrayDeque for most single-threaded queues, stacks, and deques. It uses a resizable array and prohibits null elements. LinkedList permits null, but avoid null in queues because poll() and peek() use it to signal emptiness.
Reserve LinkedList for a demonstrated need for its combined List/Deque behavior or iterator-local editing. Concurrent deque implementations exist, including LinkedBlockingDeque; the Deque interface itself doesn't determine synchronization.
The Map Interface
A Map associates unique keys with values; different keys may share a value. In a mutable map, put() with an existing key replaces its mapped value rather than rejecting a duplicate insertion.
// Fragment for Main.main
Map<String, Integer> ages = new LinkedHashMap<>();
ages.put("Sam", 33);
ages.put("Erica", 49);
ages.put("Ted", 12);
System.out.println(ages.size()); // 3
System.out.println(ages.get("Erica")); // 49
ages.remove("Ted");
System.out.println(ages.isEmpty()); // false
System.out.println(ages.containsKey("Erica"));// true
System.out.println(ages.containsValue(33)); // true
for (String key : ages.keySet()) {
System.out.println("Key: " + key);
}
for (Integer value : ages.values()) {
System.out.println("Value: " + value);
}
for (Map.Entry<String, Integer> entry : ages.entrySet()) {
System.out.println(entry.getKey() + ", " + entry.getValue());
}Map Iteration and Backed Views
keySet(), values(), and entrySet() return backed views, not snapshots. On this mutable map, removing through a view removes the corresponding mapping, and map changes appear in the views. They don't support adding mappings through add() or addAll().
Use entrySet() when you need both the key and value. This example uses LinkedHashMap so traversal follows insertion order; HashMap provides no ordering guarantee. The official map operations guide explains these views, and our Java HashMap examples cover additional usage patterns.
Modern Map Operations
The Map API includes methods that replace repetitive lookup-and-update code:
- getOrDefault(key, fallback) uses the fallback only when no mapping exists; a mapped null remains null.
- putIfAbsent(key, value) inserts when the key is absent or mapped to null.
- computeIfAbsent(key, function) computes a value for an absent or null-mapped key; a null result isn't stored.
- compute(key, function) recomputes a mapping; returning null removes it if present.
- merge(key, value, function) combines an existing non-null value with the supplied non-null value; a null remapping result removes the entry.
- forEach(action) visits key/value pairs without an explicit entry loop.
// Fragment for Main.main
Map<String, Integer> counts = new LinkedHashMap<>();
for (String word : List.of("java", "collections", "java")) {
counts.merge(word, 1, Integer::sum);
}
System.out.println(counts.getOrDefault("missing", 0)); // 0
counts.putIfAbsent("queues", 0);
counts.compute("queues", (key, value) -> value + 1);
counts.forEach((word, count) -> System.out.println(word + "=" + count));
Map<String, List<String>> teams = new HashMap<>();
teams.computeIfAbsent("platform", key -> new ArrayList<>()).add("Sam");These methods aren't automatically atomic on every Map implementation. Don't modify the same map from inside a computation callback. The map-and-lambda guide develops these aggregation patterns further.
Map Implementations and Access-Order Caching
Choose HashMap for ordinary key lookup, TreeMap for sorted keys and range queries, or LinkedHashMap for predictable encounter order. LinkedHashMap uses insertion order by default; access order must be explicitly enabled.
In an access-order map, get(), getOrDefault(), put(), putIfAbsent(), compute(), computeIfAbsent(), computeIfPresent(), and merge() count as accesses when the entry exists afterward. replace() counts when it replaces a value, and putAll() accesses each supplied mapping. Collection-view traversal doesn't count as access.
// Fragment for Main.main: small, single-threaded LRU-style cache
Map<String, String> cache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > 2;
}
};
cache.put("a", "A");
cache.put("b", "B");
cache.get("a"); // a becomes most recently accessed
cache.put("c", "C"); // b is evicted
System.out.println(cache); // {a=A, c=C}removeEldestEntry() is an overridable eviction hook, not automatic bounded-cache behavior. This example supplies a size policy but provides neither expiry nor thread safety. Use a dedicated cache design when you need those features.
Special-purpose alternatives include EnumMap for enum keys, WeakHashMap for weakly referenced keys, and IdentityHashMap for reference-identity comparisons. They aren't interchangeable substitutes for ordinary value-based maps.
Choosing a Java Collection Implementation
Use this table as a starting point. Here, n is the collection size, expected hash costs assume suitable hash distribution, and amortized costs spread occasional resizing across many operations.
| Implementation | Order | Null policy | Important costs | Choose it for |
|---|---|---|---|---|
| ArrayList | Positional | Allowed | get/set O(1); append amortized O(1); middle insertion/removal O(n) | General-purpose lists and indexed access |
| LinkedList | Positional | Allowed | End operations O(1); indexed access O(n); iterator-local insertion/removal O(1) | Specific List/Deque or iterator-editing workloads |
| ArrayDeque | Head to tail | Rejected | End insertion/removal amortized O(1); search O(n) | Non-concurrent FIFO queues and LIFO stacks |
| HashSet | Unspecified | One null allowed | add/remove/contains expected O(1) | Uniqueness without ordering requirements |
| LinkedHashSet | Insertion by default | One null allowed | add/remove/contains expected O(1) | Uniqueness with predictable encounter order |
| TreeSet | Natural or Comparator order | Rejected with natural ordering; Comparator may permit it | add/remove/contains O(log n) | Sorted unique elements and ranges |
| HashMap | Unspecified | One null key; null values allowed | get/put expected O(1) | General-purpose key lookup |
| LinkedHashMap | Insertion by default; optional access order | One null key; null values allowed | get/put expected O(1) | Predictable traversal or simple access-order eviction |
| TreeMap | Natural or Comparator key order | Null values allowed; null keys require a Comparator that permits them | get/put/remove/containsKey O(log n) | Sorted keys and range queries |
These ordinary mutable implementations aren't synchronized. Big-O also omits allocation, cache locality, resizing, hash computation, and comparator costs. Benchmark your actual operation mix and data sizes rather than assuming a universal winner; extra object allocation can also affect Java garbage collection.
Object Ordering: Comparable and Comparator
A class implements Comparable<T> to define its natural ordering. compareTo() returns a negative number, zero, or a positive number when the receiver sorts before, equivalently to, or after the argument. Integer implements Comparable, so natural sorting produces ascending numeric order:
// Fragment for Main.main
List<Integer> numbers = new ArrayList<>(List.of(3, 1));
Collections.sort(numbers);
System.out.println(numbers); // [1, 3]Use an external Comparator for alternate orderings or types you don't control. Comparing fields with Integer.compare() avoids subtraction overflow.
// Fragment for Main.main
record Person(String name, int age) implements Comparable<Person> {
@Override
public int compareTo(Person other) {
int byName = name.compareTo(other.name);
return byName != 0 ? byName : Integer.compare(age, other.age);
}
}
List<Person> people = new ArrayList<>(
List.of(new Person("Sam", 33), new Person("Erica", 49)));
people.sort(Comparator.naturalOrder());
Comparator<Person> byAgeThenName = Comparator
.comparing(Person::age)
.thenComparing(Person::name);
people.sort(byAgeThenName);
List<String> names = new ArrayList<>(Arrays.asList("Sam", null, "Erica"));
names.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(names); // [null, Erica, Sam]Orderings must be consistent and transitive. Natural ordering is strongly recommended to be consistent with equals(): comparison returning zero should normally mean equality. TreeSet and TreeMap treat comparison-zero elements or keys as equivalent, so comparing people only by age would collapse different people of the same age.
SortedSet, SortedMap, and Range Queries
SortedSet adds range views, first()/last(), and comparator(). SortedMap offers corresponding key-based operations such as subMap(), headMap(), tailMap(), firstKey(), and lastKey(). TreeSet and TreeMap implement these contracts and their navigable extensions.
// Fragment for Main.main
SortedSet<Integer> numbers = new TreeSet<>(List.of(3, 1, 6));
System.out.println(numbers); // [1, 3, 6]
System.out.println(numbers.subSet(1, 6)); // [1, 3]
System.out.println(numbers.first()); // 1
System.out.println(numbers.last()); // 6
System.out.println(numbers.comparator()); // null: natural orderingThe two-argument subSet() includes the lower bound and excludes the upper bound. Sorted range operations return backed views. Sorting a List changes its current sequence; a sorted set or map maintains its ordering as elements or mappings change.
Sequenced Collections: First, Last, and Reversed Views
Java 21 introduced SequencedCollection, SequencedSet, and SequencedMap to represent defined encounter order consistently.
- SequencedCollection provides getFirst(), getLast(), reversed(), and optional addFirst(), addLast(), removeFirst(), and removeLast() operations.
- SequencedSet combines uniqueness with sequenced behavior. LinkedHashSet and sorted sets expose this contract.
- SequencedMap provides firstEntry(), lastEntry(), pollFirstEntry(), pollLastEntry(), reversed(), and optional putFirst()/putLast(), plus sequenced key, value, and entry views.
// Fragment for Main.main; requires Java 21 or later
List<String> words = new ArrayList<>(List.of("first", "second"));
List<String> backwards = words.reversed();
System.out.println(words.getFirst()); // first
backwards.addFirst("third");
System.out.println(words); // [first, second, third]
SequencedMap<String, Integer> ranks = new LinkedHashMap<>();
ranks.putLast("a", 1);
ranks.putFirst("b", 2);
System.out.println(ranks.firstEntry().getKey()); // breversed() returns a view, not a reversed copy. Collection endpoint getters throw NoSuchElementException when empty; SequencedMap.firstEntry() and lastEntry() return null when empty. Sorted implementations reject explicit endpoint insertion that would bypass their ordering: TreeSet.addFirst() and TreeMap.putFirst() throw UnsupportedOperationException.
Unmodifiable Collections, Copies, and Views
List.of(), Set.of(), and Map.of() arrived in Java 9; the corresponding copyOf() factories arrived in Java 10. They create unmodifiable containers, not deeply immutable object graphs. The official unmodifiable collections guide explains the distinction.
// Fragment for Main.main
List<String> fixed = List.of("hello", "world");
Set<String> unique = Set.of("hello", "world");
Map<String, Integer> counts = Map.of("hello", 1);
List<String> mutable = new ArrayList<>(fixed);
List<String> snapshot = List.copyOf(mutable);
List<String> view = Collections.unmodifiableList(mutable);
mutable.add("again");
System.out.println(snapshot); // [hello, world]
System.out.println(view); // [hello, world, again]- The of() factories reject null elements, keys, and values. Set.of() rejects duplicate elements; Map.of() rejects duplicate keys. List.of() permits duplicates.
- copyOf() results don't reflect later structural changes to their source. Set.copyOf() collapses duplicate elements rather than rejecting them.
- Collections.unmodifiableList(), unmodifiableSet(), and unmodifiableMap() wrap backing collections. Changes through another mutable reference remain visible.
- All these approaches retain references to contained objects; mutable objects can still change.
Don't assume Set.of() or Map.of() preserves argument order. Also, Arrays.asList() is a fixed-size array-backed list: replacing elements is supported, but adding and removing elements isn't.
Equality, Hashing, and Stable Keys
Equal objects must return equal hash codes; unequal objects may share a hash code. Override equals() and hashCode() together for value-based objects used in hash collections. Our guide to Java equals() and hashCode() explains that contract.
Don't mutate fields participating in equality or hashing while an object is a HashMap key or HashSet element. Lookup can stop finding it even though the object remains stored. Prefer stable value keys; the official immutable-key guide illustrates the failure mode.
The same principle applies to fields used for tree ordering: remove an element or mapping before changing its ordering fields, then reinsert it.
Concurrency: Choose More Than a Thread-Safe Class
Fail-fast iterators detect some invalid modifications on a best-effort basis. ConcurrentModificationException is bug detection, not synchronization, and can occur even within one thread.
- ConcurrentHashMap supports concurrent access and atomic per-key updates such as compute() and merge(). It rejects null keys and values; separate operations or multi-key invariants aren't automatically atomic.
- CopyOnWriteArrayList suits read-heavy, rarely modified lists. Updates copy the backing array; iterators traverse a snapshot and don't support mutation.
- ConcurrentLinkedQueue provides an unbounded, thread-safe FIFO queue.
- BlockingQueue adds waiting operations such as put() and take(), plus timed offer() and poll(). ArrayBlockingQueue is bounded; LinkedBlockingQueue can be constructed with an explicit capacity.
- LinkedBlockingDeque provides blocking, thread-safe operations at both ends.
- Collections.synchronizedList() and related wrappers require all access through the wrapper. Traversal must additionally synchronize on that wrapper; compound operations need an appropriate shared lock.
A concurrent map doesn't make mutable objects stored inside it thread-safe. For example, placing ArrayList values in ConcurrentHashMap doesn't make concurrent additions to those lists safe. For producer/consumer systems, choose queue capacity and an overload policy deliberately rather than allowing backlog to grow without a bound.
Algorithms in the Collections Utility Class
Collections supplies algorithms and wrappers, not a collection implementation. reverse(), shuffle(), and sort() operate on lists; min(), max(), frequency(), and disjoint() work with collections more generally.
// Fragment for Main.main
List<Integer> numbers = new ArrayList<>(List.of(3, 1, 6));
Collections.reverse(numbers);
System.out.println(numbers); // [6, 1, 3]
Collections.shuffle(numbers);
System.out.println(numbers); // random order; no fixed expected output
Collections.sort(numbers);
System.out.println(numbers); // [1, 3, 6]
System.out.println(Collections.binarySearch(numbers, 3)); // 1
System.out.println(Collections.min(numbers)); // 1
System.out.println(Collections.max(numbers)); // 6
System.out.println(Collections.frequency(numbers, 3));// 1
System.out.println(Collections.disjoint(numbers, List.of(9))); // truebinarySearch() requires a list already sorted under the same ordering used for the search. A negative result indicates absence and encodes the insertion point. Mutating algorithms require the relevant list modification operations to be supported.
For non-mutating transformations, a stream pipeline can be clearer. Choose based on whether you intend to modify the existing collection or produce a separate result.
Common Mistakes
- Relying on hash traversal order: Use an explicitly ordered implementation when output, tests, or serialization require an order.
- Confusing copies with views: Review subList(), map views, reversed(), and unmodifiable wrappers before retaining or exposing them.
- Using list.remove(1) to remove the integer value 1: That overload removes index 1. Use list.remove(Integer.valueOf(1)) to remove a matching value.
- Choosing LinkedList from Big-O alone: Account for traversal, node allocation, and locality, not just the final pointer update.
- Depending on fail-fast exceptions: Use synchronization or an appropriate concurrent collection instead.
- Ignoring mutation and ownership: Decide who can change the container and its elements before returning it from an API.
FAQ
What is the Java Collections Framework?
The Java Collections Framework is a set of interfaces, implementations, and algorithms for storing and processing groups of objects and key/value mappings. Its core abstractions include List, Set, Queue, Deque, and Map.
What is the difference between Collection and Collections in Java?
Collection is an interface representing a group of elements and extends Iterable. Collections is a utility class with static algorithms and wrappers, including sort(), reverse(), and unmodifiableList(); Map is a separate interface, not a subtype of Collection.
When should I use ArrayList instead of LinkedList?
Use ArrayList for most lists, especially when you need indexed access, iteration, or appending. Consider LinkedList only for a specific workload benefiting from its combined List/Deque behavior or edits through an already-positioned iterator; ArrayDeque is usually the better starting point for queues and stacks.
What is the difference between HashSet, LinkedHashSet, and TreeSet?
HashSet provides uniqueness without a guaranteed encounter order, LinkedHashSet normally preserves insertion order, and TreeSet maintains natural or Comparator order. Hash-based membership operations are expected constant time under suitable hashing, while TreeSet's basic membership operations are logarithmic.
What are SequencedCollection and SequencedMap in Java?
Introduced in Java 21, these interfaces represent collections and maps with defined encounter order and consistent endpoint and reversed-view operations. SequencedSet combines sequenced behavior with Set uniqueness; reversed() returns a view rather than an independent copy.
Further Reading
- Dev.java Collections Framework guide — interface-by-interface explanations and exercises.
- Collection API contract — optional operations, views, and shared methods.
- Choosing ArrayList versus LinkedList — complexity, locality, and allocation trade-offs.
- Map API contract — lookup, computation, merging, and collection views.
- SequencedCollection API — encounter order, endpoints, and reverse views.
- Creating unmodifiable lists, sets, and maps — factories, copies, and mutable contents.
- BlockingQueue API — bounded queues, waiting, and timeout semantics.
- Collections utility API — algorithms and synchronized or unmodifiable wrappers.
