You are currently viewing Java Collections: Choosing Lists, Sets, Maps, and Queues

Java Collections: Choosing Lists, Sets, Maps, and Queues

An array has a fixed length. If a program reads an unknown number of log entries, you have to choose a capacity in advance or write resizing code. An ArrayList handles resizing for you and provides operations such as add, get, and remove. It is part of the Java Collections Framework, which supplies interfaces, implementations, and utility methods for working with groups of objects.

Different collection types solve different problems. A List keeps a sequence, a Set keeps distinct elements, and a Map associates keys with values. The useful question is not “Which class should I memorize?” but “What must this group of data do?”

Interfaces describe behavior; classes provide it

Collection<E> is a core interface for groups of elements. List<E>, Set<E>, and Queue<E> extend it with more specific behavior. Map<K,V> is also part of the framework, but it is not a subtype of Collection: it manages key–value entries rather than individual elements.

Classes such as ArrayList, HashSet, and HashMap implement those interfaces. The angle brackets specify generic types. For example, List<String> tells the compiler that the list holds strings, so retrieving an element does not require a cast. Collections hold object references, not primitives directly; use a wrapper such as Integer for int values. Java often converts between a primitive and its wrapper automatically, a feature called autoboxing.

List<String> events = new ArrayList<>();
events.add("login accepted");
events.add("session ended");
System.out.println(events.get(0)); // login accepted

Declaring events as a List<String> lets you change its implementation later. Declare it as an ArrayList<String> only when the code needs an ArrayList-specific operation. These types are in java.util; add the appropriate imports to your source file.

Collection types compared beside a Java program

Start with the behavior you need

Need Common choice Important behavior
Ordered sequence with indexed access ArrayList Duplicates allowed; positions start at zero
Distinct elements without an ordering guarantee HashSet Duplicate additions do not add another element
Distinct elements in sorted order TreeSet Elements follow natural order or a comparator
Value lookup by unique key HashMap Putting an existing key replaces its value
First-in, first-out processing ArrayDeque as a Queue Add at one end and remove from the other

These are starting points, not universal winners. An array still makes sense when the size is fixed or an API expects one. When ordering, equality, or null values matter, check the rules of the collection you choose.

Lists: a sequence that can change size

A list preserves positional order and allows duplicates. An ArrayList is backed by an array, so get(index) is fast. Adding at the end is typically efficient; inserting or removing near the beginning shifts later elements. An invalid index throws IndexOutOfBoundsException.

List<String> steps = new ArrayList<>();
steps.add("collect");
steps.add("review");
steps.add("review");
steps.set(0, "gather");
System.out.println(steps.size()); // 3

LinkedList is another List implementation. It can help with certain insertion patterns through an iterator, but finding a position may require walking the list. For an ordinary indexed list, start with ArrayList unless measurements or requirements point elsewhere. If you need a queue, use the Queue interface instead of picking a list for the job.

Sets: membership without duplicates

A Set answers questions like “Have we seen this identifier already?” A HashSet typically makes additions and membership checks fast, but its iteration order is unspecified. An order you observe in one run is not a guarantee.

Set<String> labels = new HashSet<>();
System.out.println(labels.add("warning")); // true
System.out.println(labels.add("warning")); // false
System.out.println(labels.contains("warning")); // true

Use LinkedHashSet when insertion order matters. For sorted iteration, TreeSet uses natural ordering or a supplied Comparator; its operations generally cost more than hash-based lookups. A set cannot replace a list if you need positions or repeated values.

Maps: values retrieved through keys

A Map associates each distinct key with one value. In a HashMap, putting a value under an existing key replaces the old value. Maps work well for lookup tables and counts, but you cannot keep two independent entries under the same key.

Map<String, Integer> counts = new HashMap<>();
counts.put("warning", 1);
counts.merge("warning", 1, Integer::sum);
System.out.println(counts.get("warning")); // 2

get returns null if a key has no mapping. It can also return null when the key is explicitly mapped to null, so use containsKey if you need to tell those cases apart. HashMap makes no promise about iteration order. LinkedHashMap preserves insertion order, while TreeMap orders entries by key.

Queues: process items in a deliberate order

A Queue models pending work. With ArrayDeque, offer adds an item, poll removes and returns the head, and peek examines the head without removing it. On an empty queue, poll and peek return null. ArrayDeque does not permit null elements, so that signal is unambiguous.

Queue<String> pending = new ArrayDeque<>();
pending.offer("parse file");
pending.offer("write report");
System.out.println(pending.poll()); // parse file

ArrayDeque also works as a stack through push, pop, and peek. If priority matters more than arrival order, PriorityQueue retrieves elements according to their ordering. Iterating over it, however, does not produce a sorted sequence.

Iteration, sorting, and utility methods

An enhanced for loop works well when you only need to read elements. For a map, iterate over entrySet() if you need both keys and values. To remove elements while traversing a collection, use an explicit iterator and its remove method. Calling the collection’s remove inside an enhanced for loop can cause many standard iterators to detect the structural change and throw ConcurrentModificationException.

List<String> names = new ArrayList<>(
    List.of("Ada", "", "Lin"));
Iterator<String> it = names.iterator();
while (it.hasNext()) {
    if (it.next().isEmpty()) {
        it.remove();
    }
}

List.of creates an unmodifiable list, so this example copies it into an ArrayList before removing anything. Calling add or remove on the list returned directly by List.of throws UnsupportedOperationException. A final reference is different: it prevents reassignment of the variable, not mutation of the collection it refers to.

The Collections utility class provides sort, reverse, and shuffle for suitable lists. You can also call sort on a list and pass a comparator. For instance, Comparator.comparingInt(String::length) sorts strings by length. Sorting changes the list in place; copy it first if you need to keep the original order.

A hand-drawn guide to lists sets and maps

Equality determines what “the same” means

List.contains and most set and map operations rely on equality, not on whether two references point to the same object. Strings with equal text compare as equal. For your own class, the inherited Object.equals compares identity unless you override it.

Hash-based collections have an additional rule: equal objects must return the same hashCode. Override equals and hashCode together when a custom object needs value-based identity. Do not change fields used by either method while the object is a HashMap key or a HashSet element; changing its hash can make an existing entry unexpectedly hard to find. A Java record is often a convenient immutable key when its components capture the identity you intend.

Keep collection behavior separate from security decisions. A map can count events for a report, but a count does not prove that a log line is authentic or that two similarly named accounts belong to the same user. Normalize identifiers only according to the system’s documented rules.

A small exercise that reveals the differences

In a local test program, read these event labels in order: login, error, login. Store them in an ArrayList<String> to keep all three occurrences. Add them to a LinkedHashSet<String> to get login and error once each, in first-seen order. Then use a HashMap<String, Integer> with merge(label, 1, Integer::sum) to count two logins and one error. When printing the counts, look up each label in the set’s order rather than iterating over the hash map, which offers no display-order guarantee.