LearnFrenzy
Sign in Take a free test

Control Flow and Collections · Lesson 10 of 41

Apex Collections: List, Set and Map

6 min read Updated Nov 8, 2026 Saurabh Samir

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[]');

Debug log

(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

Debug log

3 unique of 4
b unsubscribed? true
send to: {a@acme.com, c@acme.com}
in both: {b@acme.com}
account ids: 0

Map

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);

Debug log

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)');
}

Debug log

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);

Debug log

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

  1. 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)); }

  2. 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.

Next lesson

Search LearnFrenzy

Esc

Quick links