Create your own
Lesson illustration

Choosing the Right Java Collection for Ordering, Uniqueness, and Lookup

Hello. In the last lesson, you separated a service’s business workflow from the construction of its collaborators. Collection choice uses the same general discipline: express the behavior your code needs, then choose an implementation whose guarantees and costs fit that behavior.

This lesson gives you a practical way to select Java collections based on three questions: Does order matter? Must elements be unique? Do I need to find a value by a key? You will also connect those semantic requirements to the usual implementations—ArrayList, sets, maps, queues, and sorted structures—without treating Big O notation as a substitute for understanding the workload.


Start with semantics, not class names

It is tempting to begin with “Should I use ArrayList or LinkedList?” That is usually the wrong first question. First decide what the data means in the application.

For example, consider several backend requirements:

  • “Return product search results in the ranking order produced by the search service.”
    This is an ordered sequence, duplicates are conceivable, and callers may paginate or access by position. Start with a List, usually an ArrayList.

  • “Accept each requested notification channel once, while preserving the order the client supplied.”
    This requires uniqueness and insertion order. Use a LinkedHashSet.

  • “Find a customer record from its customer ID.”
    The ID is a key identifying a value. Use a Map<CustomerId, Customer>.

  • “Always process the most urgent scheduled job next.”
    This is neither ordinary insertion order nor lookup by identifier. Use a PriorityQueue.

The collections framework has two main families: Collection, for groups of elements, and Map, for associations between keys and values.

The core Java Collections Framework interfaces: `List`, `Set`, `Queue`, and `Deque` extend `Collection`; `Map` is a separate key-value hierarchy with its own sorted variant.

Storing Data Using the Collections Framework - Dev.java

Read Dev.java’s “Storing Data Using the Collections Framework” for a compact overview of the decision points before choosing an implementation.

In the section “Finding Your Way in the Collections Framework,” begin with the opening paragraph and read the selection questions. Then continue with the paragraphs beginning “There are two main categories of interfaces” through the distinction between collections and maps. Focus on the difference between selecting an interface for required behavior and selecting an implementation for its operational characteristics.

A useful rule is:

Choose the narrowest interface that captures the required behavior, then choose the implementation that provides suitable ordering and performance.

So write this when your code needs an ordered sequence:

List<OrderSummary> summaries = new ArrayList<>();

The variable is a List because clients need list operations. ArrayList is the chosen implementation because it is a strong default for that behavior.

Do not declare every variable as Collection merely because it can hold elements. A Collection reference intentionally hides operations such as get(index), so it would be too weak if indexed access is part of the contract.


Ordering has several meanings

Interview answers become much clearer when “order” is made precise. Java structures may provide one of these different guarantees:

RequirementMeaningTypical choice
Encounter or insertion orderIteration follows the order elements were addedArrayList, LinkedHashSet, LinkedHashMap
Sorted orderIteration follows natural ordering or a supplied ComparatorTreeSet, TreeMap
Priority removal orderEach removal returns the highest-priority item, not necessarily the earliest addedPriorityQueue
No ordering guaranteeCode must not depend on iteration orderHashSet, HashMap

“No ordering guarantee” does not mean a HashMap iterates randomly. It means its iteration order is an implementation detail and may change as entries are added, removed, or the table resizes. If clients need a stable order in an API response or an audit-oriented workflow, use an implementation that promises one.

A PriorityQueue needs special care. Its poll() operation returns the element with the best priority according to its natural order or comparator. But iterating over a PriorityQueue does not produce every element in sorted order. It is designed to answer “what should be processed next?”, not “show me all elements in sorted order.”

Java Collections Explained (with examples)

Watch Visual Computer Science’s “Java Collections Explained (with examples)” for a visual overview of the hierarchy and the practical roles of lists, priority queues, maps, and sets.

Watch the overview to see the framework positioned as implementations of familiar data structures. Then watch ArrayList basics for its dynamic-array model and its suitability for indexed access. Skip to priority queues, focusing on why priority determines removal order. Finish with maps and sets to compare hash-based, insertion-ordered, and tree-based choices.


Ordered sequences: List and the usual ArrayList

A List represents an ordered sequence. It permits duplicates and gives each element a position.

List<String> deliveryAttempts = new ArrayList<>();

deliveryAttempts.add("email");
deliveryAttempts.add("sms");
deliveryAttempts.add("email");  // Valid: lists allow duplicates.

String firstAttempt = deliveryAttempts.get(0);

For ordinary backend code, ArrayList is normally the best default implementation:

  • It retains insertion order.
  • It provides fast indexed access: get(index) is .
  • Appending is amortized: most appends are cheap, while occasional resizing requires copying its backing array.
  • Iteration is efficient because elements are represented in an array.
  • It uses substantially less per-element overhead than a linked list.
  • Searching for an arbitrary element with contains is , because it must scan.

The word amortized matters. An ArrayList occasionally grows its internal array and copies elements, which is for that particular append. Spread over many appends, though, the average append cost remains constant.

If you know you will load roughly 10,000 records into a temporary list, you may reduce resizing by giving an initial capacity:

List<OrderSummary> summaries = new ArrayList<>(10_000);

That is a small optimization only when the expected size is credible. Do not guess a huge capacity and waste heap space merely to avoid a resize that may never matter.

Why LinkedList is rarely the answer

LinkedList also implements List, so the APIs look similar. Its internal model is different: each element is held in a node that links to neighboring nodes. This makes indexed access expensive because Java must traverse nodes to reach a middle position.

The familiar simplified rule is:

  • ArrayList: fast random indexed access; middle insertions/removals require shifting elements.
  • LinkedList: fast insertion/removal only when the relevant node or end is already reached; random indexed access requires traversal.

The omitted phrase—“when the relevant node is already reached”—is the important one. Calling linkedList.add(middleIndex, value) still has to walk to that index, so it is . In real JVM workloads, pointer chasing and poorer cache locality also make LinkedList less attractive than its textbook story suggests.

Choosing the Right Implementation Between ArrayList and LinkedList - Dev.java

Read Dev.java’s comparison to connect Big O notation with the actual operations that make one list implementation preferable to another.

In “Algorithm Complexity,” read the subsection “Algorithm Complexity for Some Common List Operations” and its table. Start at the operation comparison. Then go to “Which Implementation Should You Choose?” and read the opening discussion through the recommendation about stacks, queues, and ArrayDeque. Focus on the conclusion: ArrayList is the default for a regular list; choosing LinkedList solely because “insertion is fast” ignores the cost of locating the insertion point.

When the real requirement is “add and remove at either end,” do not model it as a general-purpose list. Model it as a Deque:

Deque<WorkItem> pending = new ArrayDeque<>();

pending.addLast(firstJob);
pending.addLast(secondJob);

WorkItem nextJob = pending.removeFirst();

ArrayDeque is generally preferable for a non-concurrent queue or stack. It supports efficient work at both ends and avoids the legacy Stack class.


Uniqueness: select the right kind of Set

A Set models a collection in which an element appears at most once. It answers a fundamentally different business question from a List.

Suppose an order may contain several quantities of the same SKU. A List<LineItem> may be correct because repetition has meaning. But a set of enabled features should not contain "EXPRESS_CHECKOUT" twice. A Set<Feature> matches that invariant.

HashSet: unique values with fast membership checks

Use HashSet when you require uniqueness and frequently ask whether something is present, but you do not require a traversal order.

Set<String> permittedRoles = new HashSet<>();

permittedRoles.add("ADMIN");
permittedRoles.add("SUPPORT");
permittedRoles.add("ADMIN"); // Ignored: already present.

boolean allowed = permittedRoles.contains("SUPPORT");

Adding and checking membership are on average. This makes a HashSet useful for deduplicating input, checking membership, or avoiding repeat processing within a local operation.

However, HashSet depends on elements having correct equals and hashCode behavior. For domain objects, that is not an incidental technicality—it determines what “duplicate” means. The next lesson examines that contract in depth.

LinkedHashSet: unique values in insertion order

Use LinkedHashSet when you need both properties:

Set<String> requestedFields = new LinkedHashSet<>();
requestedFields.add("id");
requestedFields.add("createdAt");
requestedFields.add("id");

Iteration yields id, then createdAt. This is useful when deduplicating user input but retaining its original encounter order, such as a client-supplied list of export columns.

TreeSet: unique values in sorted order

Use TreeSet when elements must be unique and iteration needs to be sorted:

Set<String> supportedCountries = new TreeSet<>();
supportedCountries.add("DE");
supportedCountries.add("BR");
supportedCountries.add("CA");

// Iteration order: BR, CA, DE

Operations such as add, contains, and remove are . The cost buys sorted order and useful navigational operations, such as finding the nearest value before or after a given value.

A TreeSet uses natural ordering or a supplied Comparator to decide ordering and, in effect, whether two values occupy the same position. Therefore, the comparator must represent the uniqueness rule you intend. If a comparator considers two distinct domain objects equal, a TreeSet will retain only one of them.

If you need sorted output with duplicates preserved, use a List and sort it. A set would silently discard repeated values.


Lookup by identifier: Map

A Map<K, V> represents a relationship: a key identifies one associated value. Keys are unique; values do not have to be.

Map<UUID, Customer> customersById = new HashMap<>();

customersById.put(customer.id(), customer);

Customer customer = customersById.get(requestedCustomerId);

This is more expressive and efficient than storing customers in a list and scanning each time for a matching ID.

// Avoid repeated linear scanning when lookup by ID is the real operation.
Customer found = null;
for (Customer candidate : customers) {
    if (candidate.id().equals(requestedCustomerId)) {
        found = candidate;
        break;
    }
}

Use the map variants for the same ordering decisions seen with sets:

NeedImplementationKey behavior
Fast average lookup, order irrelevantHashMapget, put, and containsKey are on average
Fast average lookup plus insertion-order iterationLinkedHashMapIteration follows insertion order
Keys sorted, range or nearest-key queries usefulTreeMapKey operations are

For example, a TreeMap<Instant, Price> is reasonable if code must find the price effective immediately before a requested time. A HashMap is better if all you need is direct lookup by a known exact identifier.

As with HashSet, HashMap relies on the correctness of key equality and hashing. Mutable keys are especially dangerous: if a key’s hash-relevant state changes after insertion, a later lookup may fail even though the object is physically still in the map.


A compact selection routine

Before choosing a class, walk through these constraints in order:

  1. Is this a key-value association?
    If yes, start with Map, not List or Set.

  2. If it is a group of values, can the same logical value appear more than once?
    If yes, start with List. If no, start with Set.

  3. What ordering must callers observe?
    Use ordinary hash-based structures when no order is required, linked hash structures for insertion order, and tree structures for sorted order.

  4. Is the main operation direct position access, membership testing, key lookup, or next-item processing?

    • Indexed sequence access: usually ArrayList
    • Membership: usually HashSet
    • Key lookup: usually HashMap
    • FIFO or LIFO work: ArrayDeque
    • Best-next-by-priority work: PriorityQueue
  5. Only then consider performance details and expected data size.
    The cost of the operations your application actually performs matters more than a memorized complexity table.

Unless you deliberately select a concurrent implementation, assume the standard implementations discussed here are not thread-safe. A HashMap is not made safe for shared mutation merely by wrapping a few calls in hopeful application logic; concurrent access needs an explicit design, which will be addressed in the concurrency module.


Interview-quality explanation

For the prompt, “How do you choose between Java collections?”, avoid a catalogue of class names. State the decision process and give an example:

I start with the semantics. If I need a sequence where duplicates and position matter, I use a List, usually ArrayList, because it preserves encounter order and provides efficient indexed access. If each value should appear once, I use a Set: HashSet for average constant-time membership checks, LinkedHashSet when insertion order is part of the requirement, or TreeSet when sorted iteration is required. If the operation is finding a value by identifier, I use a Map, typically HashMap, rather than repeatedly scanning a list. I only choose LinkedList when the workload truly operates at the ends; for queues and stacks I normally prefer ArrayDeque. The trade-off is that hash-based structures do not promise iteration order, while sorted tree structures pay logarithmic operation cost to maintain ordering.

A concrete follow-up strengthens the explanation:

For a REST endpoint returning ranked search results, I would use ArrayList because result order and pagination positions matter. For a per-request set of IDs already processed, I would use HashSet because I need fast duplicate detection but no order. For customers indexed by customer ID, I would use HashMap<UUID, Customer> because lookup by key is the central operation.


Key takeaways

Collection choice is chiefly a modeling decision:

  • Use a List for an ordered sequence that permits duplicates; ArrayList is the normal default.
  • Use a Set when each logical value must be unique.
  • Use a Map when a key identifies a value.
  • Use hash-based implementations when fast average lookup or membership checking matters and no iteration order is required.
  • Use linked hash implementations when insertion order matters.
  • Use tree-based implementations when sorted order or navigational queries justify operations.
  • Use ArrayDeque for ordinary queues and stacks, and PriorityQueue when removal should honor priority.
  • Avoid selecting LinkedList based only on the slogan that inserts are cheap; reaching a middle position is itself costly.

Next, you will examine the equals and hashCode contract—the rule that makes “uniqueness” and key lookup reliable in HashSet and HashMap.

Can't find a good explanation? Sign up and we'll make it for you

Sign up