You are currently viewing Java Collections Framework: Lists, Sets, Maps, Queues, and Safe Usage

Java Collections Framework: Lists, Sets, Maps, Queues, and Safe Usage

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.

Java code using lists maps and sets

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(...), and Map.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.

Different Java collection types organized by purpose

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 ConcurrentHashMap for shared maps with concurrent access.
  • Use blocking queues from java.util.concurrent for 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.