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?
A List of Account Names only, without Ids
A Set of Contact records stored as strings
A Map<Id, Account> populated by one SOQL query using Account Ids collected from Trigger.new
A nested List of Lists with no keys, searched linearly for every Contact
Why is a Set often used when gathering AccountId values from Trigger.new before a SOQL query?
Sets preserve insertion order better than Lists for SOQL binds
Sets store each Id only once, reducing duplicate keys in the IN clause and clarifying unique parents
Sets are the only collection type allowed in SOQL bind variables
Sets automatically query child records without SOQL
What does map.get(someId) return when someId is not present as a key?
null
An empty sObject of the map’s value type
A runtime exception in all cases
The string 'missing'
Sections you finish are checked off in the contents.