---
title: "List, Set and Map: choosing correctly"
url: "https://trailassembly.com/apex-collections-choosing/"
topic: "Apex and Development"
published: "2026-09-18T02:37:55+00:00"
updated: "2026-10-06T13:57:37+00:00"
---

# List, Set and Map: choosing correctly

**Use a List when order matters or duplicates are legitimate, a Set when membership is the only question you ask, and a Map when you look things up by a key.** The decision that actually costs you money is calling `contains()` on a List inside a loop. That turns a linear scan into a quadratic one, and it surfaces as CPU time rather than as a clean limit exception, so nobody knows where it came from.

## What each one is for

- **List** keeps insertion order, allows duplicates, supports index access, and is the only one of the three you can pass straight to DML.

- **Set** holds unique values, has no index, and answers `contains()` by hash rather than by scanning.

- **Map** holds key to value pairs with unique keys, and is how you avoid a [query inside a loop](https://trailassembly.com/soql-101/).

Most Apex needs all three in the same method. Gather ids into a Set, query into a Map, build a List for the DML.

## The cost of contains() on a List

`List.contains()` walks the list from the start until it finds a match. On a list of 5,000 processed ids, checking each of 5,000 incoming records means up to 25 million comparisons. Your synchronous CPU budget is 10,000 ms, and this is a very effective way to spend it.

```apex
// Slow. Scans the list on every iteration.
List<Id> seen = new List<Id>();
for (Contact c : records) {
    if (!seen.contains(c.AccountId)) {
        seen.add(c.AccountId);
    }
}

// Fast. Hash lookup, and the uniqueness is free.
Set<Id> seen = new Set<Id>();
for (Contact c : records) {
    seen.add(c.AccountId);
}
```

The second version is shorter as well as faster, because a Set already refuses duplicates. Whenever you find yourself writing a guard before an `add`, you have picked the wrong collection.

`indexOf()` has the same problem and the same fix. If you genuinely need the position of an element, keep a `Map<Object, Integer>` alongside the list.

## Ordering guarantees, and the lack of them

A List preserves the order you put things in, and `sort()` will reorder it predictably. That is the whole guarantee list.

Salesforce does not guarantee the iteration order of a Set, or of a Map’s `keySet()` and `values()`. In practice you will often observe insertion order, which is exactly why this bites people: the code works for two years and then a platform change reorders something and a report comes out wrong. If order matters, convert to a List and sort it.

```apex
List<String> keys = new List<String>(codeToLabel.keySet());
keys.sort();
for (String k : keys) {
    // deterministic, every time
}
```

One exception is worth knowing: `new Map<Id, Account>([SELECT ...])` gives you a map whose `values()` follow the query’s `ORDER BY`. That is convenient. It is still not something to build a sorted report on.

## Key equality is where the surprises live

Three behaviours catch people out.

**Id keys normalise case and length.** A `Set<Id>` or `Map<Id, X>` treats the 15 and 18 character forms of the same record as the same key. A `Set<String>` holding the same values does not, and that mismatch is a classic integration bug.

**String keys are case sensitive.** SOQL comparison usually is not. So a map built from query results keyed on an email address will miss a lookup done with different capitalisation. Normalise with `toLowerCase()` when you build the map, and again when you read it.

**SObject keys compare every field.** Two Account instances are equal as set members only if all their populated fields match. Key on the Id instead, always.

## Memory, briefly

Heap is 6 MB synchronous and 12 MB asynchronous. A Map of full sObject records is the usual way to exhaust it. Query only the fields you need, and if you are caching for a lookup, cache the one field you actually read rather than the whole record. A `Map<Id, String>` costs a fraction of a `Map<Id, Account>`.

Verified in a Summer ’26 scratch org on 14 September 2026. Inserting `z`, `a`, `m` in that order and printing the collection gives `{a, m, z}` for both a `Map<String,…>` keySet and a `Set<String>`. String keys come back **sorted**, not in insertion order. Do not rely on either behaviour, and never on insertion order.

## Frequently asked

**Is a Set faster than a Map for membership?** They perform the same. Pick the Set when you have nothing to store against the key, because the code reads better.

**Can I sort a Set?** No. Copy it into a List first. `new List<String>(mySet)` does that in one line.

**Does removing from a List inside a for loop work?** Not reliably, and it is the source of skipped elements. Build a second List of the survivors instead.

**Do collection operations count against a governor limit?** Only CPU and heap. There is no limit on the number of map lookups, which is precisely why trading queries for lookups is the standard move.

---

Source: https://trailassembly.com/apex-collections-choosing/
Trail Assembly — independent Salesforce write-ups.
