An Apex List is an ordered collection of elements that allows duplicates and is accessed by a zero-based index. It is the most-used collection in Salesforce because every SOQL query returns a List of sObjects. The methods you will reach for daily are add(), get() (or bracket notation myList[0]), set(), size(), isEmpty(), contains(), indexOf(), remove(), addAll(), clear(), sort(), and clone()/deepClone(). Lists also power bulkification: you collect records into a List and run a single DML statement so your code stays inside Salesforce governor limits.
This guide covers how to declare a List, every method that matters with short examples, custom sorting with Comparable and Comparator, and how List fits alongside Set and Map in the Apex collections trio.
Key takeaways
- A
List<T>is ordered (by insertion), allows duplicates, and is index-based (zero-based). - SOQL returns a
Listof sObjects, so Lists appear in almost every Apex class and trigger. - Core methods:
add,add(index, e),addAll,get,set,size,isEmpty,clear,contains,indexOf,remove(index),sort,clone,deepClone,equals. - Sort sObjects with
sort(); sort custom classes by implementingComparable, or pass aComparator(added in recent releases) tosort()for reusable, multi-field ordering. - The #1 reason Lists matter: collect records in a
Listand do ONE DML (insert myList) instead of DML inside a loop, to respect governor limits. - Use a
Listfor ordered data, aSetfor uniqueness, and aMapfor key-value lookups.
What is an Apex List in Salesforce?
A List is a typed, resizable, ordered collection. Three properties define it:
- Ordered - elements keep the position in which you inserted them.
- Allows duplicates - the same value can appear many times.
- Index-based - you read or replace any element by its zero-based index.
Elements can be any data type: primitives (String, Integer, Decimal), sObjects (Account, Contact), Apex classes, or even other collections (a List of Lists).
You declare a List with new List<DataType>() - empty, or seeded inline with curly braces. And because a SOQL query always returns a List of sObjects (even when one row matches), you will assign query results to a List constantly:
// Declare an empty List, then add later
List<String> names = new List<String>();
// Initialize with values inline
List<String> fruits = new List<String>{ 'apple', 'banana', 'cherry' };
// index 0 = 'apple', index 1 = 'banana', index 2 = 'cherry'
// A nested (multidimensional) List
List<List<String>> grid = new List<List<String>>();
// A SOQL query always returns a List of sObjects
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Industry = 'Technology'];
System.debug('Rows returned: ' + accounts.size());How do you add and access elements?
add(element)- appends an element to the end.add(index, element)- inserts atindex, shifting later elements right.addAll(fromList)/addAll(fromSet)- appends every element from another List or Set of the same type.get(index)- returns the element atindex(bracket notationmyList[index]does the same).set(index, element)- replaces the element atindex.
List<String> names = new List<String>();
names.add('John'); // ['John']
names.add('Kelly'); // ['John', 'Kelly']
names.add(0, 'Mike'); // insert at index 0 -> ['Mike', 'John', 'Kelly']
names.addAll(new List<String>{ 'Paul', 'Sara' }); // append another List
String first = names.get(0); // 'Mike'
String alsoFirst = names[0]; // bracket notation works too -> 'Mike'
names.set(1, 'Jonathan'); // replace index 1 -> 'John' becomes 'Jonathan'How do you check size, search, and remove elements?
size()- number of elements (Integer).isEmpty()-truewhensize()is 0.contains(element)-trueif the value is present.indexOf(element)- index of the first match, or -1 if absent.remove(index)- removes and returns the element atindex.clear()- removes every element.
List<Integer> nums = new List<Integer>{ 10, 20, 30, 20 };
Integer count = nums.size(); // 4
Boolean empty = nums.isEmpty(); // false
Boolean has20 = nums.contains(20);// true
Integer idx = nums.indexOf(20); // 1 (first match)
Integer removed = nums.remove(0); // returns 10 -> list is now [20, 30, 20]
nums.clear(); // [] -> size() == 0How do you sort, copy, and compare a List?
sort()- sorts in place in natural (ascending) order. For primitives this is numeric/alphabetical; for sObjects it sorts on the field used in the query (or the first sortable field).clone()- returns a shallow copy. For a List of sObjects the new List has new references but points at the same records.deepClone(preserveId, preserveReadonlyTimestamps, preserveAutonumber)- copies the sObject records themselves (sObjects only).equals(list)- element-by-element comparison;==does the same.
// sort() orders in place
List<Integer> nums = new List<Integer>{ 3, 1, 2 };
nums.sort(); // -> [1, 2, 3]
// clone() = shallow copy (sObject references are shared)
List<Account> originals = [SELECT Id, Name FROM Account LIMIT 5];
List<Account> shallow = originals.clone();
List<Account> deep = originals.deepClone(true, true, true); // copies the records
// equals() / == compare element by element
List<Integer> a = new List<Integer>{ 1, 2 };
List<Integer> b = new List<Integer>{ 1, 2 };
Boolean same = a.equals(b); // true (a == b is also true)How do you sort by a custom field or rule?
Default sort() is fine for primitives and simple sObject lists, but to sort a List of your own Apex objects - or sObjects by a non-default rule - you have two options:
- Implement the
Comparableinterface (onecompareTo()method) on the class you are sorting.sort()then uses that logic. - Pass a
Comparatorinstance tosort(comparator). TheComparatorinterface was added in recent Salesforce releases (Winter '24) and lets you keep sort logic outside the data class, so one type can be sorted many ways.
// Option 1: Comparable - the class knows how to compare itself
public class OppWrapper implements Comparable {
public Opportunity opp;
public OppWrapper(Opportunity o) { this.opp = o; }
// Required by Comparable: return -1, 0, or 1
public Integer compareTo(Object compareTo) {
OppWrapper other = (OppWrapper) compareTo;
if (opp.Amount == other.opp.Amount) return 0;
return opp.Amount > other.opp.Amount ? 1 : -1;
}
}
List<OppWrapper> wrappers = new List<OppWrapper>();
for (Opportunity o : [SELECT Id, Amount FROM Opportunity]) {
wrappers.add(new OppWrapper(o));
}
wrappers.sort(); // uses compareTo()
// Option 2: Comparator - reusable, external sort logic (Winter '24+)
public class AmountComparator implements Comparator<Opportunity> {
public Integer compare(Opportunity a, Opportunity b) {
if (a.Amount == b.Amount) return 0;
return a.Amount > b.Amount ? 1 : -1;
}
}
List<Opportunity> opps = [SELECT Id, Amount FROM Opportunity];
opps.sort(new AmountComparator());Apex List method reference
| Method | Signature | What it does |
|---|---|---|
| add | void add(Object e) |
Append an element to the end |
| add (at index) | void add(Integer i, Object e) |
Insert at index i, shifting the rest right |
| addAll | void addAll(List l) / void addAll(Set s) |
Append all elements from a List or Set |
| get | Object get(Integer i) |
Return the element at index i |
| set | void set(Integer i, Object e) |
Replace the element at index i |
| size | Integer size() |
Number of elements |
| isEmpty | Boolean isEmpty() |
true when size is 0 |
| contains | Boolean contains(Object e) |
true if the value is present |
| indexOf | Integer indexOf(Object e) |
Index of first match, or -1 |
| remove | Object remove(Integer i) |
Remove and return element at index i |
| clear | void clear() |
Remove all elements |
| sort | void sort() / void sort(Comparator c) |
Sort in place, natural order or by a Comparator |
| clone | List clone() |
Shallow copy |
| deepClone | List deepClone(Boolean, Boolean, Boolean) |
Deep copy of sObject records |
| equals | Boolean equals(List l) |
Element-by-element equality |
Why do Lists matter for bulkification and governor limits?
This is the single most important reason to master Lists. Salesforce is multi-tenant, so it enforces governor limits per transaction - for example, a maximum of 150 DML statements and 100 SOQL queries synchronously. Code that issues DML or a query inside a loop burns through those limits the moment it processes a realistic batch of records (triggers fire on up to 200 records at a time, and Data Loader sends thousands).
The fix is the bulkification pattern: loop once to build a List, then run a single DML statement on the whole List. The same principle applies to queries - query once into a List (or Map) before the loop instead of querying inside it. For the full picture, read our deep dive on Salesforce governor limits and the basics of how to run a SOQL query in Salesforce.
// ANTI-PATTERN: DML inside a loop - fails on large data volumes
for (Account a : accountsToUpdate) {
a.Active__c = true;
update a; // one DML statement per iteration
}
// BULKIFIED: collect into a List, then ONE DML statement
List<Account> toUpdate = new List<Account>();
for (Account a : accountsToUpdate) {
a.Active__c = true;
toUpdate.add(a);
}
update toUpdate; // a single DML for the entire List
// insert / update / delete / upsert all accept a List
List<Contact> newContacts = new List<Contact>{
new Contact(LastName = 'Smith'),
new Contact(LastName = 'Jones')
};
insert newContacts;List vs Set vs Map: which Apex collection should you use?
List, Set, and Map are the three Apex collection types. They solve different problems:
| Collection | Ordered? | Duplicates? | Accessed by | Best for |
|---|---|---|---|---|
| List | Yes (insertion order) | Yes | Index (get(i)) |
Ordered data, SOQL results, batching records for DML |
| Set | No | No (unique values) | Membership (contains) |
Collecting unique values, de-duplicating Ids, fast lookups |
| Map | No (key order not guaranteed) | Unique keys | Key (get(key)) |
Key-to-value lookups, grouping records by a field such as Id |
A common real-world flow uses all three: query a List from SOQL, pull unique parent Ids into a Set, then build a Map keyed by Id for O(1) lookups inside a loop. For the other two collections, see our companion guides on how to use Sets in Salesforce for uniqueness and de-duplication, and Apex Map methods in Salesforce for key-value lookups.
// 1. SOQL returns a List
List<Contact> contacts = [SELECT Id, AccountId FROM Contact WHERE AccountId != null];
// 2. Collect unique parent Ids in a Set (no duplicates)
Set<Id> accountIds = new Set<Id>();
for (Contact c : contacts) {
accountIds.add(c.AccountId);
}
// 3. Build a Map for fast key lookups (one query, not one-per-contact)
Map<Id, Account> accountById = new Map<Id, Account>(
[SELECT Id, Name FROM Account WHERE Id IN :accountIds]
);
for (Contact c : contacts) {
Account parent = accountById.get(c.AccountId); // O(1) lookup, no SOQL in loop
System.debug(c.Id + ' belongs to ' + parent.Name);
}Frequently Asked Questions
Does a SOQL query return a List in Apex?
Yes. A SOQL query always returns a List of sObjects, even if only one record matches. You can assign the result to a single sObject variable as shorthand, but that throws a QueryException if zero or more than one row is returned, so assigning to a List is the safe default.
What is the difference between a List and a Set in Apex?
A List is ordered, allows duplicate values, and is accessed by index. A Set is unordered, stores only unique values, and is optimised for membership checks with contains(). Use a List when order or duplicates matter (such as SOQL results), and a Set when you need uniqueness, like collecting distinct record Ids.
How do I sort a List of sObjects or custom objects in Apex?
For a List of sObjects, calling sort() orders them on the field used in the query. To sort a List of your own Apex classes, implement the Comparable interface and define compareTo(). For reusable or multi-field ordering, pass a Comparator instance to sort(comparator) - the Comparator interface was added in recent Salesforce releases (Winter '24).
Why should I avoid DML inside a loop?
Salesforce enforces governor limits, including a cap of 150 DML statements per transaction. DML inside a loop issues one statement per record and quickly hits that limit on real data volumes. Instead, collect the records into a List and run a single DML statement (for example update myList). This bulkification pattern is the main reason Lists are central to Apex.
What is the difference between clone() and deepClone() for a List?
clone() makes a shallow copy: the new List has its own references, but for sObjects those references still point at the same underlying records. deepClone() (sObjects only) copies the records themselves, with flags to control whether Ids, read-only timestamps, and auto-numbers are preserved. Use deepClone() when you need genuinely independent record copies.
Is there a size limit on an Apex List?
There is no fixed maximum number of elements, but Lists count against the Apex heap-size limit: 6 MB in synchronous transactions and 12 MB in asynchronous ones. For very large data volumes, process records in batches (for example with Batch Apex) rather than loading everything into one List.
Build robust Apex on Salesforce
Mastering Lists - and the bulkification pattern they enable - is the difference between Apex that passes a unit test and Apex that survives a 50,000-record data load. At MicroPyramid we have delivered Salesforce solutions for 12+ years across 50+ projects, writing governor-limit-safe triggers, services, and integrations that scale.
Need a hand with Apex, triggers, or a Salesforce build? Explore our Salesforce development services and let's talk about your project.