Mastering Apex Collections | Salesforce Developers Blog

This topic was originally presented by Philippe Ozil during an Apex Hours session on September 4, 2021.

Working with collections like List, Map, and Set is part of an everyday routine for Apex developers. While their basic use is straightforward, there are some advanced quirks that can get you to the next level. In this post, we’ll start with the basics about objects and collections, then we’ll dive into advanced concepts, such as iterators and sorting.

Exploring objects and collections

About objects and their methods

In Apex, all classes and primitives inherit from the Object class. This inheritance is implicit in the sense that you do not need to specify it when creating a custom class, for instance.

Thanks to the inheritance from Object, all object instances can use the equals, hashCode, and toString methods as demonstrated here:

Boolean myBoolean = false;
Object myObject = (Object) myBoolean; // Boolean is a child of Object
System.debug(myObject.equals(true));  // false
System.debug(myObject.hashCode());    // 1237
System.debug(myObject.toString());    // false

equals let you compare an object instance for equality with another one. What’s interesting with this method is that it lets you compare object of different types. For instance, you can compare an Id with a String:

Id accountId = '0010o00002svksw'; // 15-character ID
String accountIdString = '0010o00002svkswAAA'; // Same ID in 18-character format
accountId.equals(accountIdString); // true

While equals is powerful, it has a performance cost when running on objects with large number of properties. This is where the hashCode method comes into play. hashCode returns an integer that identifies an object instance based on some of its property values. For example, the integer 123 has a hash code of 123 and a “Hello World” string has a hash code of -862545276.

Hash code values are pseudo-unique as some object instances with different property values may share the same hash code. This is called hash code collision and is considered acceptable. When a collision happens, equals is used as a fallback to compare objects. In any case, comparing two objects’ hash codes is statistically faster than calling equals.

The equals and hashCode methods are essential when working with collections, and we’ll see why as we explore the different collection types.

An overview of collection types

Object instances can be stored in three types of collections: List, Set, and Map. These collection types have different purposes and properties:

  • A List holds an ordered collection of non-unique elements
  • A Set holds an unordered collection of unique elements
  • A Map holds a dictionary of key values in which keys are unique but values are not

Fun fact: Set and Map are named after mathematical concepts (see Set and Map definitions).

List and Set have some degree of interoperability as they share some constructors and methods that work with both types. For example, you can build a List from a Set and vice-versa, and you can call methods like addAll with both types. This enables powerful uses cases, such as removing duplicate elements from a List by casting it into a Set:

// Casting a List into a Set removes duplicate elements
// at the expense of list's order
List<Integer> duplicates = new List<Integer>{ 5, 1, 3, 1, 5, 3 };
List<Integer> uniqueItems = new List<Integer>(new Set<Integer>(duplicates));
System.debug(uniqueItems); // (5, 1, 3)

The unicity of Set values and Map keys is enforced thanks to the hashCode method and a fallback to equals in case of hash code collision. These two methods are also essential for some Set and Map operations, such as Set.contains or Map.containsKey. Interestingly, List operations like List.contains solely rely on equals and never on hashCode.

Now that we’ve seen the big picture on objects and collection types, let’s dive into collection specifics. We’ll start by iterating on Lists and Sets.

Iterating on Lists and Sets

List and Set can be traversed with for loops, but there’s a powerful alternative: iterators. List and Set implement the Iterable interface, and this interface provides access to an iterator method that exposes an object that implements the Iterator interface.

Thanks to the Iterator’s hasNext and next methods, you can traverse a collection in a single direction:

List<Account> myList = new List<Account>{ /* some accounts */ };
Iterator<Account> it = myList.iterator();
while (it.hasNext()) {
    Account acc = it.next();
    // Do something
}

While the above code is more verbose than for loops, using iterators has three key advantages:

  • Iterators provide read-only access to collections. If you pass an iterator as a method parameter, you’re sure that this method cannot modify your collection.
  • Iterators lock the collection in read-only mode, preventing any modification to the collection while you are iterating on it:
Set<Integer> mySet = new Set<Integer>{ 1, 2, 3 };
Iterator<Integer> it = mySet.iterator();

it.next();
it.next();

mySet.add(4); // Fails with System.FinalException because iterator is in use

it.next();
  • Iterators provide a mechanism that let you dynamically retrieve elements on the go. Our core focus in this article is collections, but you can implement your own iterable classes. This is extremely useful when working with paginated data and when you retrieve elements in batches. For example, you could build a REST client that iterates on a resource, and you could start iterating without knowing the exact number of items that are available.

Check out iteration recipes in Apex Recipes for iterator examples along with their related test class.

Sorting Lists

The first choice when it comes to sorting records is generally to write a SOQL query with an ORDER BY clause. However, sometimes you need to sort records without fetching them from the database or even sort objects that are not records. In those cases, you can use the List.sort method like this:

List<String> colors = new List<String>{ 'Yellow', 'Red', 'Green' };
colors.sort(); // Sorts list of string alphabetically
System.debug(colors); // (Green, Red, Yellow)

The List.sort method is easy to use, but let’s take a closer look at how it works and how you can customize the ordering.

Working with the Comparable interface

List.sort works with List elements that implement the Comparable interface. The interface specifies a single method: compareTo that is called by the List sorting algorithm to order elements.

compareTo returns an integer with the following values:

  • 0 if this instance and objectToCompareTo are equal
  • > 0 if this instance is greater than objectToCompareTo
  • < 0 if this instance is less than objectToCompareTo

List.sort can sort any mix of objects from various types as long as they are primitives or they implements Comparable. For instance, you could perfectly write something like this:

List<Object> myList = new List<Object>{
  4, 3.14, true, null, 'World', 'Hello', 1L, false, Date.today()
};
myList.sort(); // null, false, 4, 3.14, true, 1, 2021-09-03 00:00:00, Hello, World

As a word of caution, it’s safe to place objects that don’t implement Comparable in a List, but if you call the sort method on that List, you’ll get a System.ListException exception.

The sort method documentation provides an example implementation for sorting a custom class, so we won’t dive in details on this topic. But what about sorting SObject lists (list of records)?

Sorting lists of sObjects

SObject implements the Comparable interface and instances of that class have a predictable sort order. However, you may need to implement a custom sort order in some cases, and this is where things get more complex. The SObject class is final, so you cannot overwrite its internal methods, such as compareTo.

To compensate for that, you must work with a wrapper class around the SObject that you want to sort. Imagine that you are importing some accounts from a third-party integration and that you want to sort them based on the shipping country field before saving them. You can’t sort the records with SOQL since they are not yet in the database. You must implement the following class:

public class SortableAccount implements Comparable {
  private final Account account;

  public SortableAccount(Account account) {
    this.account = account;
  }

  public Integer compareTo(Object otherObject) {
    // For additional type safety, check if otherObject is a SortableAccount
    // if not, throw a SortException
    if (!(otherObject instanceof SortableAccount)) {
      throw new SortException('Can\'t sort with incompatible type');
    }
    // Cast otherObject to SortableAccount and compare it
    SortableAccount other = (SortableAccount) otherObject;
    if (this.account.ShippingCountry < other.account.ShippingCountry) {
      return -1;
    }
    if (this.account.ShippingCountry > other.account.ShippingCountry) {
      return 1;
    }
    return 0;
  }
  
  public class SortException extends Exception {}
}

You could sort your List with your custom ordering logic in three steps:

  1. Convert the List<Account> into a List<SortableAccount>
  2. Call the sort method on List<SortableAccount>
  3. Convert the List<SortableAccount> back to List<Account>

However, implementing these steps is not practical as it would almost take as many lines of code as the implementation of SortableAccount and those extra lines wouldn’t be reusable. Fortunately, there’s something that you can do to reduce boilerplate code. Simply add the following static method to SortableAccount:

public static void sort(List<Account> accounts) {
    // Convert List<Account> into List<SortableAccount>
    List<SortableAccount> sortableAccounts = new List<SortableAccount>();
    for (Account acc : accounts) {
      sortableAccounts.add(new SortableAccount(acc));
    }

    // Sort accounts using SortableAccount.compareTo
    sortableAccounts.sort();

    // Overwrite the account list provided in the input parameter
    // with the sorted list. Doing this avoids a return statement
    // and is less verbose for the method user.
    for (Integer i = 0; i < accounts.size(); i++) {
      accounts[i] = sortableAccounts[i].account;
    }
}

With this SortableAccount.sort static method, all it takes to sort a list of account records by shipping country is a single line:

SortableAccount.sort(accounts);

Check out List recipes in Apex Recipes for the implementation of SortableAccount along with the related test class.

Sorting Lists with reusable comparators

While the default List.sort method is convenient, it has two important limitations:

  • The ordering logic is directly tied to the Comparable object that is being sorted
  • The sort method lacks the ability to sort with different strategies and parameters

The Java language (which is close to Apex) goes beyond the basic Apex sort method and exposes a convenient Arrays.sort(T[], Comparator) method where T is the type being sorted and Comparator an interface that specifies a compare method that works like Comparable.compareTo.

This pattern can easily be replicated in Apex with a custom ListUtils class and a Comparator interface:

With this approach, you can sort Lists with different comparators and even pass parameters to comparators. You benefit from the fact that the ordering logic is decoupled from the objects that you sort.

// Sort accounts alphabetically based on shipping country
ListUtils.sort(accounts, new SObjectStringFieldComparator('ShippingCountry'));

// Sort accounts alphabetically based on industry
ListUtils.sort(accounts, new SObjectStringFieldComparator('Industry'));

// Sort accounts based on rating values
// as defined in the rating picklist order (non-alphabetical sort)
ListUtils.sort(accounts, new AccountRatingComparator());

Check out List recipes in Apex Recipes for the implementation of ListUtils, some comparators, and related test classes.

Closing words

That’s a wrap. We gave you a refresher on the Object class and the different collection types. We covered advanced collection concepts: you’ve learned how to use iterators and how to sort Lists with the default sort method and custom comparators. We hope this article helped to improve your understanding of collections. It’s now your turn to put this knowledge into practice in your projects.

Resources

About the author

Philippe Ozil is a Principal Developer Advocate at Salesforce where he focuses on the Salesforce Platform. He writes technical content and speaks frequently at conferences. He is a full stack developer and enjoys working on DevOps, robotics, and VR projects. Follow him on Twitter @PhilippeOzil or check his GitHub projects @pozil.

Stay up to date with the latest news from the Salesforce Developers Blog

Subscribe