One of the most frequent debates among Salesforce developers revolves around a seemingly simple task: checking if a list has records. You’ve likely seen variations like if(myList.size() > 0), if(!myList.isEmpty()), or the classic if(myList != null). While they might appear interchangeable, choosing the wrong one can lead to redundant code, unnecessary CPU consumption, or the dreaded NullPointerException (NPE).

In this guide, we will break down the technical nuances of each method, explore why Salesforce-specific behaviors (like SOQL results) change the rules, and look at how modern Apex features like the Safe Navigation Operator have simplified our workflows.

The Fundamental Truth About SOQL and Nulls

Before diving into the syntax, we must address a common misconception: the belief that a SOQL query can return a null list. In Salesforce Apex, a query that finds no records will always return an empty list, never a null reference.

Consider this standard pattern:

Account[] records = [SELECT Id, Name FROM Account WHERE Name = 'NonExistentAccount'];

// This check is redundant and wastes CPU time
if(records != null && !records.isEmpty()) {
    for(Account record : records) {
        // Logic here
    }
}

In the example above, the records != null check is completely unnecessary. Furthermore, the Apex for-each loop is designed to handle empty collections gracefully. If the list is empty, the loop simply doesn't execute. For standard iterations, you often don't need any conditional check at all.

Comparing the Three Approaches

When you do need to verify a list's contents—perhaps to avoid a secondary SOQL query or to perform a specific logic branch—you have three main options. Let’s evaluate them.

1. List.isEmpty()

This is generally considered the most readable and "Pythonic" or "Java-like" way to handle collections. It returns a Boolean: true if the list has zero elements, and false otherwise.

Pros: Explicit intent; easy to read. Cons: Will throw a NullPointerException if the list variable itself is null.

2. List.size() > 0

This checks the integer count of the collection. Many developers prefer this because it feels more mathematically explicit.

Pros: Clear logic for those coming from C-style languages. Cons: Like isEmpty(), it throws an NPE if the list is null. Some argue that !myList.isEmpty() is slightly faster in certain languages, though in Apex, the performance difference is negligible.

3. List != null

This checks if the pointer to the list exists in memory. It does not tell you if there are records inside the list.

Pros: Essential for defensive programming when dealing with variables that haven't been initialized. Cons: An empty list (size 0) is still != null. Using this alone to check for data is a common logic bug.

The Modern Solution: The Safe Navigation Operator

Introduced in recent years, the Safe Navigation Operator (?.) has revolutionized how we handle potential nulls in Apex. It allows you to chain methods without fear of an NPE. If the left-hand side is null, the entire expression returns null instead of crashing.

Here is how you can use it to check for list contents safely:

// If records is null, the result is null. 
// If records is initialized, it returns the size.
if(records?.size() > 0) {
    // This block only runs if records is NOT null AND size is > 0
}

This is the most efficient way to combine a null check and a size check into a single, readable line of code.

When Should You Actually Use a Check?

As established, you don't need to check a list before a for loop. However, there is one critical scenario where a check is mandatory: Preventing unnecessary SOQL or DML operations.

Account[] accounts = [SELECT Id FROM Account WHERE Industry = 'Technology'];

// Early return to save governor limits
if(accounts.isEmpty()) {
    return;
}

// Only perform the second query if we actually have Account IDs to filter by
Contact[] contacts = [SELECT Id, Name FROM Contact WHERE AccountId IN :accounts];

In this case, using isEmpty() is a best practice for performance. It prevents the platform from executing a Contact query that would inevitably return zero results, saving you precious SOQL limit units.

Defensive Programming and Utility Methods

In large codebases, you might encounter lists from various sources: method parameters, Visualforce controllers, or LWC wire adapters. In these cases, you cannot always guarantee initialization.

One popular approach is to use a global utility method to handle these checks consistently:

public class CollectionUtils {
    public static Boolean isNotEmpty(List<SObject> sobjectList) {
        return sobjectList != null && !sobjectList.isEmpty();
    }
}

By using a generic List<SObject> parameter, you can pass in lists of Accounts, Opportunities, or custom objects, making your validation logic reusable across the entire org.

Frequently Asked Questions

Does myList.size() == 0 perform differently than myList.isEmpty()?

In Apex, there is no significant performance difference between the two. isEmpty() is often preferred for readability, as it returns a Boolean directly, whereas size() == 0 requires a comparison operation.

Why does my list check fail on a Multi-Select Picklist?

Multi-select picklists in Apex are often returned as a List<String>. If no values are selected, the variable might be null rather than an empty list. In this specific scenario, you must check if(selectedValues != null) before attempting to process the list.

Can I use the Safe Navigation Operator on Map keys?

Yes! The operator works across all collection types. For example, myMap.get(someKey)?.someMethod() will safely handle cases where the key doesn't exist or the value is null.

Wrapping Up

To write the most efficient Apex possible, follow these three rules: 1. Trust SOQL: Don't null-check the results of a query; it will never be null. 2. Initialize Early: Always initialize your lists (e.g., List<Contact> contacts = new List<Contact>();) to avoid the need for null checks later. 3. Use Safe Navigation: When you are unsure if a list is initialized (such as in an input parameter), use if(myList?.size() > 0) for a clean, crash-proof check.

By standardizing how you handle collections, you reduce boilerplate code and make your logic significantly easier for other developers to maintain.