Critical update: “Predictable Iteration Order for Apex Unordered Collections” documentation

Today in one of my sandboxes, I was overjoyed to see the following critical update:
“Predictable Iteration Order for Apex Unordered Collections” – The iteration order of unordered collections (maps and sets) is now deterministic.

But… I’m not 100% sure what this means. Has anyone seen any documentation on this? I Googled it and found this article. It says:

This update makes your code more robust because the iteration order in unordered collections is always the same.

But it’s not clear what this exactly means – do Sets and Maps now maintain order like a List? Or does it still come out ‘randomly’ but the same random order every time?

Answer

I wasn’t satisfied with the accepted answer, specifically relating to the VisualForce aspect of it, so I performed some additional testing on this critical fix with regard to VF.

What I learned is that when a VF iterator (apex:pageBlockTable, apex:repeat, etc.) is used against a Map, one of its undescribed actions is to sort the keys. This completely negates the benefits of a deterministic order being present.

In this test, I queried for 8 contact records and sorted them by Last & First in descending order into a List<Contact> as the “source of truth” and then iterated the list and populated a few maps to be shown on-screen.

The results rendered on-screen using an iterator that did not leverage the keyset were displayed in the expected order. An iterator that used the keyset produced “undesired” results.

While testing, I attempted to use a complex type for the key and received an enlightening error message from the VF runtime that: This map cannot be used in an iteration because the keys cannot be sorted. (aha!) In an attempt to get around this behavior I implemented the Comparable interface on the apex class to allow sorting of the complex type key – which got past the first error but still did not work and threw another unrelated show-stopping VF error. (So, I gave up.)

In the end, while the order may be deterministic in Apex, when used with a VisualForce iterator – the keys will be sorted on your behalf and beyond your control.

(On further consideration, I wonder if the sorting is implemented within the .keyset() method on the Map class and I am falsely accusing the VF of dirty deeds.)

Test Results Image

Test Results

Controller

public with sharing class DeterministicCollectionController {

    public List<Contact> contactsSortedByLastFirst { get; set; }

    public Map<Id, Contact> orderedContactMap { get; set; }
    public Map<String, Contact> orderedContactMapAsString { get; set; }
    public Map<String, Contact> orderedContactMapAsAnIntegerString { get; set; }
    // public Map<WrapperClass, Contact> orderedContactMapWrappers { get; set; }

    public List<Contact> orderedContactMapValues { get { return orderedContactMap.values(); } set; }

    public DeterministicCollectionController() {

        orderedContactMap = new Map<Id, Contact>();
        orderedContactMapAsString = new Map<String, Contact>();
        orderedContactMapAsAnIntegerString = new Map<String, Contact>();
        // orderedContactMapWrappers = new Map<WrapperClass, Contact>();

        populateContactMap();
    }

    public void populateContactMap() {

        // query for 8 contacts, populate the source of truth
        contactsSortedByLastFirst = [SELECT Id
                                            , FirstName
                                            , LastName
                                            , CreatedDate
                                        FROM Contact
                                        ORDER BY LastName DESC, FirstName DESC
                                        LIMIT 8 ];

        // iterate the ordered list
        system.debug('Iterating the source of truth');
        for (Integer i = 0; i < contactsSortedByLastFirst.size(); i++) {

            Contact c = contactsSortedByLastFirst[i];

            system.debug('Contact Index [' + i + ']: ' + c);

            // add them to the maps
            orderedContactMap.put(c.Id, c);
            orderedContactMapAsString.put(c.Id, c);
            orderedContactMapAsAnIntegerString.put(i + '', c);
            // orderedContactMapWrappers.put(new WrapperClass(c.Id, i), c);

        }

        List<Contact> mapValues = orderedContactMap.values();
        system.debug('Iterating the orderedContactMap values list');
        for (Integer i = 0; i < mapValues.size(); i++) {
            system.debug('Index [' + i + ']: ' + mapValues[i]);
        }

        system.debug('Serialize the map itself');
        system.debug(orderedContactMap);

        system.debug('Iterate the keyset and get the value by key');
        // output the map to the debug log as proof of order
        for (Id key : orderedContactMap.keyset()) {
            system.debug('Key: ' + key + ' Value: ' + orderedContactMap.get(key));
        }
    }

    /*
    public class WrapperClass {

        public Id theId { get; set; }
        public Integer theIndex { get; set; }

        public WrapperClass(Id anId, Integer anIndex) {
            this.theId = anId;
            this.theIndex = anIndex;
        }

        public Integer compareTo(Object compareTo) {
            WrapperClass w = (WrapperClass)compareTo;
            if (theIndex == null || w.theIndex == null || theIndex == w.theIndex) {
                return 0;
            }
            if (theIndex < w.theIndex) {
                return -1;
            } else {
                return 1;
            }
        }
    }
    */

}

VF

<apex:page controller="DeterministicCollectionController">

<style>
    table.test th { width: 12em; }
    table.test td { font-family: monospace; }
</style>

<h1>Deterministic Testing</h1><br />
<p>Query: [SELECT Id, FirstName, LastName, CreatedDate FROM Contact ORDER BY LastName DESC, FirstName DESC LIMIT 8]</p>

<h2>Source of truth: Ordered List from SOQL Query - Result: IN ORDER</h2>
<table class="test">
    <tr>
        <th>key</th><th>Id</th><th>First</th><th>Last</th><th>Created Date</th>
    </tr>
    <apex:repeat value="{!contactsSortedByLastFirst}" var="contact">
    <tr>
        <td>&nbsp;</td>
        <td>
            <apex:outputField value="{!contact.Id}" />
        </td>
        <td>
            <apex:outputField value="{!contact.FirstName}" />
        </td>
        <td>
            <apex:outputField value="{!contact.LastName}" />
        </td>
        <td>
            <apex:outputField value="{!contact.CreatedDate}" />
        </td>
    </tr>
</apex:repeat>
</table>

<hr />

<h2>Using Map&lt;Id, Contact&gt;, Iterating over keyset - Result: OUT OF ORDER</h2>
<table class="test">
    <tr>
        <th>key</th><th>Id</th><th>First</th><th>Last</th><th>Created Date</th>
    </tr>
    <apex:repeat value="{!orderedContactMap}" var="contactId">
    <tr>
        <td>{!contactId}</td>
        <td>
            <apex:outputField value="{!orderedContactMap[contactId].Id}" />
        </td>
        <td>
            <apex:outputField value="{!orderedContactMap[contactId].FirstName}" />
        </td>
        <td>
            <apex:outputField value="{!orderedContactMap[contactId].LastName}" />
        </td>
        <td>
            <apex:outputField value="{!orderedContactMap[contactId].CreatedDate}" />
        </td>
    </tr>
</apex:repeat>
</table>

<hr />

<h2>Using Map&lt;Id, Contact&gt;, Using a method to expose map.values() List directly - Result: IN ORDER</h2>
<table class="test">
    <tr>
        <th>key</th><th>Id</th><th>First</th><th>Last</th><th>Created Date</th>
    </tr>
    <apex:repeat value="{!orderedContactMapValues}" var="contact">
    <tr>
        <td>&nbsp;</td>
        <td>
            <apex:outputField value="{!contact.Id}" />
        </td>
        <td>
            <apex:outputField value="{!contact.FirstName}" />
        </td>
        <td>
            <apex:outputField value="{!contact.LastName}" />
        </td>
        <td>
            <apex:outputField value="{!contact.CreatedDate}" />
        </td>
    </tr>
</apex:repeat>
</table>

<hr />

<h2>Using Map&lt;String, Contact&gt;, Iterating Keyset made up of Ids - Result: OUT OF ORDER</h2>
<table class="test">
    <tr>
        <th>key</th><th>Id</th><th>First</th><th>Last</th><th>Created Date</th>
    </tr>
    <apex:repeat value="{!orderedContactMapAsString}" var="contactId">
    <tr>
        <td>{!contactId}</td>
        <td>
            <apex:outputField value="{!orderedContactMapAsString[contactId].Id}" />
        </td>
        <td>
            <apex:outputField value="{!orderedContactMapAsString[contactId].FirstName}" />
        </td>
        <td>
            <apex:outputField value="{!orderedContactMapAsString[contactId].LastName}" />
        </td>
        <td>
            <apex:outputField value="{!orderedContactMapAsString[contactId].CreatedDate}" />
        </td>
    </tr>
</apex:repeat>
</table>

<hr />

<h2>Using Map&lt;String, Contact&gt;, Iterating Keyset made up of stringified integers - Result: IN ORDER</h2>
<table class="test">
    <tr>
        <th>key</th><th>Id</th><th>First</th><th>Last</th><th>Created Date</th>
    </tr>
    <apex:repeat value="{!orderedContactMapAsAnIntegerString}" var="key">
    <tr>
        <td>{!key}</td>
        <td>
            <apex:outputField value="{!orderedContactMapAsAnIntegerString[key].Id}" />
        </td>
        <td>
            <apex:outputField value="{!orderedContactMapAsAnIntegerString[key].FirstName}" />
        </td>
        <td>
            <apex:outputField value="{!orderedContactMapAsAnIntegerString[key].LastName}" />
        </td>
        <td>
            <apex:outputField value="{!orderedContactMapAsAnIntegerString[key].CreatedDate}" />
        </td>
    </tr>
</apex:repeat>
</table>

<hr />

<h2>Using Map&lt;WrapperClass, Contact&gt;, Iterating Keyset made up of Apex Class - Result: In Order</h2>
<table class="test">
    <tr>
        <th>key</th><th>Id</th><th>First</th><th>Last</th><th>Created Date</th>
    </tr>
    <apex:repeat value="{!orderedContactMapWrappers}" var="key">
    <tr>
        <td>{!key}</td>
        <td>
            <apex:outputField value="{!orderedContactMapWrappers[key].Id}" />
        </td>
        <td>
            <apex:outputField value="{!orderedContactMapWrappers[key].FirstName}" />
        </td>
        <td>
            <apex:outputField value="{!orderedContactMapWrappers[key].LastName}" />
        </td>
        <td>
            <apex:outputField value="{!orderedContactMapWrappers[key].CreatedDate}" />
        </td>
    </tr>
</apex:repeat>
</table>
</apex:page>

Attribution
Source : Link , Question Author : Jason Hartfield , Answer Author : Mark Pond

Leave a Comment