6.2 Lists, Sets & Maps
Key Takeaways
- List is ordered and allows duplicates—use for query results and sequence-sensitive work
- Set stores unique elements—use to deduplicate IDs and test membership efficiently
- Map holds key→value pairs; Map<Id, sObject> is the core bulkification pattern after a single query
- Nested collections (Map of Lists, List of Maps) are valid and common for grouping related records
- Prefer one query plus Maps over SOQL inside loops; Sets prevent duplicate DML targets
6.2 Lists, Sets & Maps
Quick Answer: Apex collections are List (ordered, duplicates allowed), Set (unique elements), and Map (key→value). Platform Developer I heavily tests Map<Id, sObject> for bulkification: query once, index by Id, then process Trigger.new without SOQL or DML inside per-record loops. Use Sets to collect unique parent IDs and block duplicate DML.
Collections sit at the center of bulk-safe Apex. If you only memorize syntax, you will miss scenario questions; if you internalize when each structure wins, trigger and governor items become straightforward.
List: Ordered Sequences
A List is an ordered collection that allows duplicate values. SOQL returns List<sObject> by default.
List<Account> accounts = [SELECT Id, Name FROM Account LIMIT 100];
List<String> statuses = new List<String>{ 'New', 'Working', 'Closed' };
statuses.add('New'); // duplicates allowed
String first = statuses[0];
Common List operations: add, addAll, get / index [], size, isEmpty, clear, contains, remove, sort (when elements are comparable).
When to use List
- Preserving query order or insertion order
- Passing records to DML (
insert accounts) - Iterating every element including duplicates
- Building result sequences for return values and batch chunks
Exam tip: List index is zero-based. Accessing an invalid index throws a runtime exception—check size() or use safe iteration.
Set: Uniqueness and Membership
A Set stores unique elements. Adding a duplicate is ignored (no error, no second copy).
Set<Id> accountIds = new Set<Id>();
for (Contact c : Trigger.new) {
if (c.AccountId != null) {
accountIds.add(c.AccountId);
}
}
// accountIds has each parent Id at most once
Boolean hasId = accountIds.contains(someId);
When to use Set
- Collecting related record IDs from Trigger.new without duplicates
- Fast membership tests (
contains) before expensive work - Deduplicating strings or enums before processing
- Building SOQL bind collections:
WHERE Id IN :accountIds
Sets do not guarantee order. Do not rely on iteration order for business sequencing—use a List if order matters.
Duplicate prevention pattern: Before DML, put candidates in a Map<Id, sObject> or track Ids in a Set so the same record is not updated twice in one transaction (duplicate Id in one update list causes errors).
Map: Keys to Values (Bulkification Hero)
A Map associates keys with values. The most important exam pattern is Map<Id, sObject> after a single query:
Set<Id> accountIds = new Set<Id>();
for (Contact c : Trigger.new) {
if (c.AccountId != null) accountIds.add(c.AccountId);
}
Map<Id, Account> accountsById = new Map<Id, Account>([
SELECT Id, Name, Industry
FROM Account
WHERE Id IN :accountIds
]);
for (Contact c : Trigger.new) {
Account parent = accountsById.get(c.AccountId);
if (parent != null && parent.Industry == 'Agriculture') {
// use parent fields without extra SOQL
}
}
The constructor new Map<Id, sObject>(listOfSObjects) indexes each record by its Id automatically—an idiomatic shortcut tested often.
Why Maps bulkify code
| Anti-pattern | Bulk-safe pattern |
|---|---|
SOQL inside for (Contact c : Trigger.new) | One SOQL with WHERE Id IN :ids, then Map lookup |
| DML inside per-record loop | Build a List/Map of changes, one DML after the loop |
| Re-querying the same parent repeatedly | accountsById.get(c.AccountId) |
map.get(key) returns null when the key is missing—always null-check before using the value, or use containsKey first.
Other useful Map APIs: put, putAll, keySet(), values(), size(), isEmpty(), remove.
for (Id accId : accountsById.keySet()) {
Account a = accountsById.get(accId);
}
for (Account a : accountsById.values()) {
// iterate records directly
}
Nested Collections
You can nest collections for grouping:
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact c : [
SELECT Id, AccountId, Email FROM Contact WHERE AccountId IN :accountIds
]) {
if (!contactsByAccount.containsKey(c.AccountId)) {
contactsByAccount.put(c.AccountId, new List<Contact>());
}
contactsByAccount.get(c.AccountId).add(c);
}
Common shapes:
Map<Id, List<sObject>>— children grouped by parent IdMap<String, Set<Id>>— keys like external keys or statuses to Id setsList<Map<String, Object>>— less common; sometimes used for generic payloads
Nested collections increase heap usage—still better than exploding SOQL rows inside loops, but avoid building huge graphs when a single aggregate query would suffice.
Choosing List vs Set vs Map
| Need | Prefer |
|---|---|
| Query results / DML input order | List |
| Unique IDs / membership tests | Set |
| Lookup related record by Id in a loop | Map |
| Group children by parent | Map<Id, List<T>> |
| Both uniqueness and later DML list | Map values → List, or Set of Ids then requery |
Id uniqueness: If you only need unique Ids for a bind variable, Set is enough. If you need the full parent record fields in a loop, Map<Id, sObject> is required.
Iteration Patterns That Show Up on the Exam
- Traditional index loop —
for (Integer i = 0; i < list.size(); i++)when you need the index. - List iteration —
for (Account a : accounts)most readable for sObjects. - SOQL-for loop —
for (Account a : [SELECT ...])streams rows in chunks and is governor-friendlier for large result sets than loading one giant List (covered more with SOQL chapters). - Map key/value iteration —
keySet()/values()as shown above.
Avoid modifying a List’s structure while iterating it with a for-each loop in ways that confuse indices; build a separate “to delete” or “to update” collection instead.
Duplicate Prevention with Sets (and Maps)
Scenarios:
- Multiple Contacts in Trigger.new share one AccountId → Set collapses to one query key
- Building
List<Account> toUpdate→ useMap<Id, Account> toUpdateandputby Id so each Account appears once - Same record qualified by two rules → Map last-write or merge fields deliberately
Map<Id, Account> toUpdate = new Map<Id, Account>();
// ... business rules ...
toUpdate.put(a.Id, a); // last put wins for that Id
update toUpdate.values();
Exam Mindset
When a question shows SOQL inside a loop over Trigger.new, the fix is almost always: collect Ids in a Set, query into a Map, loop again with get. When a question asks which collection removes duplicates, answer Set (or Map keys). When order of insertion matters for processing, answer List.
A trigger must read parent Account fields for every Contact in Trigger.new without querying inside the loop. Which collection pattern is most appropriate?
Why is a Set often used when gathering AccountId values from Trigger.new before a SOQL query?
What does map.get(someId) return when someId is not present as a key?