Control Flow and Collections · Lesson 10 of 41
Apex Collections: List, Set and Map
Collections are the backbone of good Apex. Triggers hand you lists, you de-duplicate Ids with sets, and you join related records with maps instead of querying in loops. If you get fluent with these three types, writing bulk-safe code becomes natural.
In this lesson
- List: ordered, indexed, allows duplicates
- Set: unique values, fast lookups
- Map: key → value lookups
- Map<Id, SObject> straight from a query
- Grouping children by parent and sorting
Which collection?
- Listordered, by index, duplicates allowed – query results, records to save
- Setunique, unordered – Ids to query, "have we seen this?"
- Mapkey → value – look up a record by Id, counts per category
List
List<String> cities = new List<String>{ 'Pune', 'Delhi', 'Mumbai' };
cities.add('Chennai'); // append
cities.add(0, 'Bengaluru'); // insert at index
System.debug(cities + ' size ' + cities.size());
System.debug('first: ' + cities[0] + ', last: ' + cities[cities.size() - 1]);
System.debug('contains Delhi: ' + cities.contains('Delhi') + ', index: ' + cities.indexOf('Delhi'));
cities.remove(cities.indexOf('Mumbai'));
cities.sort(); // alphabetical, in place
System.debug(cities);
List<String> more = new List<String>{ 'Jaipur' };
more.addAll(cities);
System.debug(more + ', empty? ' + more.isEmpty());
String[] arrayStyle = new String[]{ 'same', 'type' }; // String[] is a List<String>
System.debug(arrayStyle.size() + ' items in a String[]');
(Bengaluru, Pune, Delhi, Mumbai, Chennai) size 5 first: Bengaluru, last: Chennai contains Delhi: true, index: 2 (Bengaluru, Chennai, Delhi, Pune) (Jaipur, Bengaluru, Chennai, Delhi, Pune), empty? false 2 items in a String[]
Indexes start at 0, and reading past the end throws ListException: List index out of bounds. Check size() or isEmpty() before using [0].
Set
List<String> emails = new List<String>{ 'a@acme.com', 'b@acme.com', 'a@acme.com', 'c@acme.com' };
Set<String> unique = new Set<String>(emails); // duplicates disappear
System.debug(unique.size() + ' unique of ' + emails.size());
Set<String> unsubscribed = new Set<String>{ 'b@acme.com', 'z@acme.com' };
System.debug('b unsubscribed? ' + unsubscribed.contains('b@acme.com'));
Set<String> toEmail = unique.clone();
toEmail.removeAll(unsubscribed); // difference
System.debug('send to: ' + toEmail);
Set<String> both = unique.clone();
both.retainAll(unsubscribed); // intersection
System.debug('in both: ' + both);
// a Set<Id> is the standard way to collect Ids for one query
List<Contact> contacts = new List<Contact>{ new Contact(LastName = 'A'), new Contact(LastName = 'B') };
Set<Id> accountIds = new Set<Id>();
for (Contact c : contacts) {
if (c.AccountId != null) accountIds.add(c.AccountId);
}
System.debug('account ids: ' + accountIds.size()); // these unsaved contacts have no AccountId yet
3 unique of 4
b unsubscribed? true
send to: {a@acme.com, c@acme.com}
in both: {b@acme.com}
account ids: 0Map
Map<String, Decimal> taxRates = new Map<String, Decimal>{ 'IN' => 0.18, 'DE' => 0.19, 'US' => 0.0 };
taxRates.put('GB', 0.20);
taxRates.put('IN', 0.18); // existing key: value replaced
System.debug('India rate: ' + taxRates.get('IN'));
System.debug('Has FR? ' + taxRates.containsKey('FR') + ', FR rate: ' + taxRates.get('FR'));
System.debug('Keys: ' + taxRates.keySet());
System.debug('Size: ' + taxRates.size());
// counting with a map
Map<String, Integer> countByStage = new Map<String, Integer>();
for (String stage : new List<String>{ 'Proposal', 'Closed Won', 'Proposal', 'Proposal' }) {
Integer current = countByStage.containsKey(stage) ? countByStage.get(stage) : 0;
countByStage.put(stage, current + 1);
}
System.debug(countByStage);
India rate: 0.18
Has FR? false, FR rate: null
Keys: {DE, GB, IN, US}
Size: 4
{Closed Won=1, Proposal=3}get() on a missing key returns null rather than throwing – combine it with ??: (countByStage.get(stage) ?? 0) + 1.
Map<Id, SObject> from a query
Pass a query (or a list of records) to the Map constructor and you get records keyed by Id in one line. This is the most useful collection pattern in Apex:
List<Account> accs = new List<Account>{
new Account(Name = 'Acme', Industry = 'Energy'),
new Account(Name = 'Globex', Industry = 'Retail')
};
insert accs;
insert new List<Contact>{
new Contact(LastName = 'Rao', AccountId = accs[0].Id),
new Contact(LastName = 'Iyer', AccountId = accs[0].Id),
new Contact(LastName = 'Shah', AccountId = accs[1].Id)
};
Map<Id, Account> accountsById = new Map<Id, Account>([SELECT Id, Name, Industry FROM Account]);
System.debug(accountsById.size() + ' accounts in the map');
// join contacts to their account without querying in a loop
for (Contact c : [SELECT LastName, AccountId FROM Contact ORDER BY LastName]) {
Account parent = accountsById.get(c.AccountId);
System.debug(c.LastName + ' works at ' + parent.Name + ' (' + parent.Industry + ')');
}
// group children by parent: Map<Id, List<Contact>>
Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>();
for (Contact c : [SELECT LastName, AccountId FROM Contact ORDER BY LastName]) {
if (!contactsByAccount.containsKey(c.AccountId)) {
contactsByAccount.put(c.AccountId, new List<Contact>());
}
contactsByAccount.get(c.AccountId).add(c);
}
for (Id accId : contactsByAccount.keySet()) {
System.debug(accountsById.get(accId).Name + ': ' + contactsByAccount.get(accId).size() + ' contact(s)');
}
2 accounts in the map Iyer works at Acme (Energy) Rao works at Acme (Energy) Shah works at Globex (Retail) Acme: 2 contact(s) Globex: 1 contact(s)
Sorting lists of records
// SortByAmountDesc.cls
public class SortByAmountDesc implements Comparator<Opportunity> {
public Integer compare(Opportunity a, Opportunity b) {
Decimal x = a.Amount ?? 0, y = b.Amount ?? 0;
return x == y ? 0 : (x > y ? -1 : 1);
}
}
List<Opportunity> opps = new List<Opportunity>{
new Opportunity(Name = 'Small', Amount = 5000),
new Opportunity(Name = 'Large', Amount = 250000),
new Opportunity(Name = 'Unknown'),
new Opportunity(Name = 'Medium', Amount = 60000)
};
opps.sort(new SortByAmountDesc());
for (Opportunity o : opps) System.debug(o.Name + ' ' + o.Amount);
List<Integer> numbers = new List<Integer>{ 42, 7, 19 };
numbers.sort();
System.debug(numbers);
Large 250000 Medium 60000 Small 5000 Unknown null (7, 19, 42)
Primitives sort with sort(). For records, pass a Comparator – or simply let SOQL sort with ORDER BY when the data comes from a query.
Practice
Try it yourself
Given a list of Opportunities, build a
Map<Id, Decimal>of total Amount per AccountId.Show answer
Map<Id, Decimal> totals = new Map<Id, Decimal>(); for (Opportunity o : opps) { totals.put(o.AccountId, (totals.get(o.AccountId) ?? 0) + (o.Amount ?? 0)); }Which collection would you use to check quickly whether an email already exists among 10,000 contacts?
Show answer
A
Set<String>of lower-cased emails –contains()is a fast lookup, unlike searching a List.
Quick check
How do you remove duplicates from a List?
Create a Set from it: new Set<String>(myList).
What does map.get() return for a missing key?
null.
How do you turn a query into records keyed by Id?
new Map<Id, Account>([SELECT … FROM Account]).
Summary
- List for ordered data, Set for uniqueness and fast contains, Map for lookups.
- Collect Ids in a Set, query once, and look records up in a Map.
- Group children with Map<Id, List<SObject>>.
- Sort primitives with sort(), records with a Comparator or ORDER BY.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.