A List that allows duplicates, a Map built for key-based lookups, and a queue that handles work in arrival order solve different problems. The Java Collections Framework provides standard interfaces and implementations for each, so you can express intent without rebuilding familiar data structures.
Collections show up in nearly every Java application: storing database results, grouping log entries, tracking permissions, buffering tasks, and removing repeated values from imported data. The choice affects correctness and readability, and it can matter for security when a collection holds untrusted input or sensitive records.
What the Java Collections Framework is
The Java Collections Framework (JCF) is a group of interfaces, classes, utility methods, and algorithms, mainly in packages such as java.util. It provides reusable ways to work with groups of objects.
The usual approach is to program against an interface:
List<String> names = new ArrayList<>();
Map<String, Integer> scores = new HashMap<>();
The declared type tells other code what it can rely on: an ordered list or a key-value mapping. If requirements change, you can often replace the implementation without changing the code that uses the variable.
Most collection types use generics. List<String> accepts strings, while Map<String, Integer> associates strings with integers. Generics catch many type errors at compile time and avoid unsafe casts.

The main collection interfaces
The framework separates behavior into interfaces. Understanding each contract is more useful than memorizing every available implementation.
Collection
Collection<E> is the broad parent interface for groups of elements. It includes common operations such as add(), remove(), contains(), size(), and iteration. Map is deliberately not a subtype of Collection, because it represents associations between keys and values rather than one group of elements.
List: ordered elements, duplicates allowed
A List preserves element order and allows duplicates. It also supports indexed access, so list.get(0) returns the first element. Use a list when position matters or repeated values have meaning.
Typical uses include:
- Lines read from a text file
- Search results displayed in rank order
- Events recorded chronologically
- A sequence of validation errors
The common implementations are ArrayList and LinkedList. An ArrayList uses a resizable array and is the normal default. It offers quick indexed reads and appends. A LinkedList connects nodes; adding or removing at either end can be efficient, but indexed access requires walking through nodes and is usually slower in real programs.
Choose ArrayList unless profiling and the actual access pattern give you a clear reason to choose another type.
Set: unique elements
A Set rejects duplicate elements according to their equality rules. Calling add() returns false if an equal value is already present.
Set<String> allowedRoles = new HashSet<>();
allowedRoles.add("admin");
allowedRoles.add("editor");
allowedRoles.add("admin");
System.out.println(allowedRoles.size()); // 2
HashSet is usually the general-purpose choice, but it does not guarantee iteration order. LinkedHashSet preserves insertion order. TreeSet keeps elements sorted through natural ordering or a supplied comparator.
Sets work well for deduplication, membership checks, tracking processed identifiers, and representing permissions or feature flags. Do not depend on the iteration order of a HashSet; its contract does not promise a particular order, and it may differ between runs or Java versions.
Queue and Deque: processing order
A Queue represents items waiting to be processed. A conventional queue follows first-in, first-out behavior: add at the tail and remove from the head. offer(), poll(), and peek() are often useful because they return special values instead of throwing exceptions when an operation cannot be completed.
A Deque is a double-ended queue, so it supports adding and removing at both ends. It can represent either a queue or a stack. ArrayDeque is a good default for local queue and stack behavior. For stacks, prefer Deque with methods such as push() and pop() instead of the older Stack class.
Map: keys associated with values
A Map<K, V> stores one value for each unique key. Keys cannot repeat, while values can. Maps fit lookups such as user ID to profile, configuration name to setting, or file extension to handler.
Map<String, String> settings = new HashMap<>();
settings.put("theme", "dark");
settings.put("language", "en");
String theme = settings.get("theme");
HashMap is the usual default and generally offers quick average-case insertion, lookup, and removal. LinkedHashMap keeps insertion order. TreeMap sorts entries by key. EnumMap is an efficient specialized choice when the keys are enum constants.
Maps provide three useful views: keySet(), values(), and entrySet(). When you need both key and value, iterate over entrySet() instead of looking up each key again:
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
Choosing an implementation
Start with the behavior you need, then consider ordering, lookup cost, and how the collection will be modified. These defaults cover many cases.
| Need | Useful default | Key behavior |
|---|---|---|
| Ordered sequence with frequent reads | ArrayList |
Fast indexed access and append |
| Unique values with fast membership checks | HashSet |
No guaranteed iteration order |
| Unique values in insertion order | LinkedHashSet |
Predictable iteration order |
| Values or keys in sorted order | TreeSet or TreeMap |
Sorted traversal, logarithmic operations |
| Fast key-to-value lookup | HashMap |
Average constant-time lookup |
| FIFO work items | ArrayDeque |
Efficient operations at both ends |
Big-O notation is a useful guide, not a complete performance promise. A HashMap lookup is expected to be O(1), while a TreeMap lookup is O(log n). Actual performance also depends on allocation, memory locality, hash quality, data size, and workload. Measure before replacing a clear default with a specialized structure.
Equality, hashing, and sorting rules
Hash-based collections—HashSet, HashMap, and related classes—depend on equals() and hashCode(). If two objects are equal according to equals(), they must return the same hash code. Violating that rule can make an element seem to disappear from a set or map.
A common and risky mistake is changing an object after it has been used as a hash map key or a hash set element. If a field used by hashCode() changes, the object may no longer be found in the bucket where it was stored. Immutable key objects are easier to reason about and less prone to this problem.
Sorted collections use natural ordering through Comparable or an explicit Comparator. A comparator should be consistent and should not rely on values that can change while objects are inside a TreeSet or TreeMap.
List<String> files = new ArrayList<>(List.of("report.txt", "README.md", "app.java"));
files.sort(String.CASE_INSENSITIVE_ORDER);
Iteration and safe modification
Most standard collection iterators are fail-fast. If code structurally modifies a collection outside its iterator while traversing it, Java may throw ConcurrentModificationException. Treat that exception as a bug detector, not as a concurrency mechanism.
When filtering a mutable collection during iteration, use the iterator's own remove() method:
Iterator<String> iterator = names.iterator();
while (iterator.hasNext()) {
if (iterator.next().isBlank()) {
iterator.remove();
}
}
For straightforward transformations, Java Streams can make the intent easier to read, but streams do not replace collections. A stream describes an operation pipeline; a collection stores data. For example, collect filtered results into a new list instead of modifying the source while streaming it.
Mutable, unmodifiable, and immutable collections
Callers should not always be able to modify a collection. Returning a mutable internal list can allow unrelated code to alter an object's state unexpectedly.
List.of(...),Set.of(...), andMap.of(...)create unmodifiable collections and reject null elements.Collections.unmodifiableList(list)creates a read-only view of an existing list. Changes to the original list are still visible through that view.List.copyOf(list)creates an unmodifiable copy and is often the better option for defensive copying.
In a constructor, defensive copying stops callers from retaining a mutable reference:
public final class Course {
private final List<String> lessons;
public Course(List<String> lessons) {
this.lessons = List.copyOf(lessons);
}
public List<String> lessons() {
return lessons;
}
}
This does not make a deep copy of mutable objects inside the list, but it protects the list structure. If the elements themselves are mutable and sensitive, use immutable element types or define explicit copying rules.

Concurrency and collection safety
Classes such as ArrayList and HashMap are not generally safe for unsynchronized writes from multiple threads. A shared collection can produce lost updates, inconsistent reads, or runtime failures when threads modify it concurrently.
Choose an approach that fits the situation:
- Keep collections thread-confined when possible: create and use them within one thread.
- Use immutable snapshots when data can be published safely without later mutation.
- Use concurrent types such as
ConcurrentHashMapfor shared maps with concurrent access. - Use blocking queues from
java.util.concurrentfor producer-consumer workflows that need coordination.
A synchronized wrapper is not always sufficient. For example, Collections.synchronizedList(...) protects individual operations, but iteration still requires external synchronization around the full traversal. Concurrent collections have their own iteration semantics, so read their contracts instead of assuming they behave like a locked ArrayList.
Practical patterns and common mistakes
Count occurrences with merge
Frequency counting is a natural map task. merge() avoids manually checking whether a key already exists:
Map<String, Integer> counts = new HashMap<>();
for (String word : words) {
counts.merge(word, 1, Integer::sum);
}
Use the interface, not an accidental implementation
Returning List<Record> tells callers they receive an ordered collection. Returning ArrayList<Record> exposes an implementation detail and makes future changes harder. Use a concrete type only when its special operations are genuinely part of the API requirement.
Do not use raw types
A declaration such as List items = new ArrayList(); disables generic type checking and can put incompatible objects in the same list. Prefer List<String> or another explicit parameterized type. The diamond operator, <>, keeps construction concise without losing type safety.
Handle null deliberately
Null handling differs by collection. HashMap allows one null key and null values, while TreeMap generally cannot order a null key with natural ordering. Factory methods such as List.of() reject null. In new code, treating null as invalid input and validating it at system boundaries usually produces clearer failures than allowing it to move through the program.
As a practical exercise, load a list of usernames, normalize each value with trim() and lowercase conversion using Locale.ROOT, then add valid names to a LinkedHashSet. Finally, create an unmodifiable List from that set. This removes duplicates while preserving first-seen order and gives downstream code a collection it cannot accidentally modify.
