18

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?

  • 1
  • Thanks - just found that myself and updated my question to reflect it. – Jason Hartfield Mar 25 '15 at 20:37
  • 1
    I think it means exactly the first part of the what you said. the order you put things into the Map or Set, is the order they will iterate through. The critical update is pretty poorly written though. It seems to say, don't write crappy code where you depend on the order, but we went ahead and made it so you could do that anyways. – CyberJus Mar 25 '15 at 20:46
  • 4
    Until we get official confirmation of what this does, you can bet I'm going to keep making Lists from KeySets, then sorting the Lists, and using that as my iterator. – sfdcfox Mar 25 '15 at 21:23
  • 1
    After applying this update to an org, it appears to only apply to Apex. After binding a PageBlockTable to the ordered Set collection, the items on the page came out in a non-deterministic order just as before the update. (Looks like it is ordered by CreatedDate) More details are definitely needed. – Mark Pond Mar 26 '15 at 19:27
11

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>
  • Mark, this is great thanks for details. We were running some tests and were trying to make sense of it. If .values is the only way to get the map in the order you started it, then there is no access to the key there, and what is the point of the map I say? Odd and still seems broken. Found that a Set is also alphabetizing the items.. just run a system.debug of the set. And run a system.debug of a map's keyset.. that is where the alphabetizing comparable seems to happen. It likely helps with performance for when you add a new set/map item, it's been alphabetized for half-interval search – Mike Katulka Jan 26 '16 at 20:26
6

My suggestion: just turn it on and test it on a sandbox or developer org. Should become pretty clear.

Update: reading MarkPonds comment on behavior in VF, I would recommend to stick with the current pattern sfdcfox describes using sorted lists from keysets as iterators. The weird and incomplete documentation might be a hint that this isn't shipped as a big feature but more as warning for those who currently relay on the old in deterministic ordering (who ever this might be...). Critical updates typically come more with a risky flavor than with the aroma of a pure feature.

There isn't much more information available yet. I just saw this alert in the partner Community with some backgrounds:

Who is impacted by this?

Any partners using Apex code that exercises order-dependent logic on unordered collections.

What is the change?

In anticipation of planned changes to Java Development Kit 8 (JDK8), Salesforce is changing the Apex implementation for unordered collections (maps and sets) to make the order of elements in those collections deterministic. With this update, the order of elements in maps and sets is the same every time your code is run again.

This update may cause a different iteration order for maps and sets than the current order. Once this change takes effect, any Apex code that exercises order-dependent logic on unordered collections can fail.

Why is this change happening?

The current underlying data type used by Apex for maps and sets is incompatible with upcoming changes being made to the Java Development Kit (JDK) for security and stability. The benefit of this update is that it makes Apex code more robust because the iteration order of unordered collections will always be the same.

What action do I need to take?

On March 24, 2015, this change will be released as a critical update in the application, which gives you control over testing and activating this change in your org. You can test and enable the change until your org is upgraded to the Summer ‘15 release*, at which point we will auto-activate any remaining orgs. Please see “Critical Updates Overview” in Salesforce Help to learn more about the critical update process.

During this period, you will need to update any Apex code or tests that rely on order-dependent logic. Prior to the auto-activation date, you can toggle between the current and new behaviors to support testing efforts.

*Currently scheduled for June 2015; date subject to change Where can I go for more information? For more information, please reference the release notes or the relevant Force.com Developer Guide topics on maps or sets. If you have further questions, please submit a case in the Partner Community.

  • How do you toggle between the two behaviors? – JimH Apr 1 '15 at 0:14
  • By enabling and disabling the critical update in setup (works only if displayed in your org under critical updates) – Uwe Heim Apr 1 '15 at 0:17
2

I went ahead and put together some tests and validated that Sets and Maps now do maintain their order like a List after the critical update is applied.

Here is a unit test I put together that compares the order of a set and map to the order of a list. When I ran it without the critical update applied, it failed. When I ran it with the critical update applied, it passed.

@isTest
private class testDeterministicMaps
{
    @isTest
    static void testOrdering()
    {
        // Given
        // Create a whole ton of random objects.  Put them in a Map and a Set and a List.  Check to see if the order is maintained.
        List<string> stringList = new List<string>();
        Map<string,string> stringMap = new Map<string,string>();
        Set<string> stringSet = new Set<string>();

        for(integer i = 0; i<100; i++){
            string iString = i+'';
            stringList.add(iString);
            stringMap.put(iString,iString);
            stringSet.add(iString);
        }

        integer i = 0;
        for(string stringSetVal : stringSet){
            string stringListVal = stringList[i];
            system.assertEquals(stringListVal,stringSetVal);
            i++;
        }

        i = 0;
        for(string stringMapKey : stringMap.keySet()){
            string stringListVal = stringList[i];
            system.assertEquals(stringListVal,stringMapKey);
            i++;
        }

        i = 0;
        for(string stringMapVal : stringMap.values()){
            string stringListVal = stringList[i];
            system.assertEquals(stringListVal,stringMapVal);
            i++;
        }

    }
}

I also put together a Visualforce page and checked the output manually. Page:

<apex:page showHeader="true" sidebar="true" controller="DeterministicController">
    List:<br/>
    <apex:repeat value="{!stringList}" var="v" >
        {!v}
    </apex:repeat>
    <br/><br/>
    Map.values():<br/>
    <apex:repeat value="{!stringMapValues}" var="v" >
        {!v}
    </apex:repeat>
    <br/><br/>
    Map.keys():<br/>
    <apex:repeat value="{!stringMapKeys}" var="v" >
        {!v}
    </apex:repeat>
    <br/><br/>
    Set:<br/>
    <apex:repeat value="{!stringSet}" var="v">
        {!v}
    </apex:repeat>
</apex:page>

Controller:

public with sharing class DeterministicController {
    public List<string> stringList {get;set;}
    public Map<string,string> stringMap {get;set;}
    public Set<string> stringSet {get;set;}
    public List<String> stringMapValues {
        get{
            return stringMap.values();
        }
    }
    public Set<String> stringMapKeys {
        get{
            return stringMap.KeySet();
        }
    }
    public DeterministicController() {
        stringList = new List<string>();
        stringMap = new Map<string,string>();
        stringSet = new Set<string>();

        for(integer i = 0; i<100; i++){
            string iString = i+'';
            stringList.add(iString);
            stringMap.put(iString,iString);
            stringSet.add(iString);
        }
    }
}

Without the update, here is what was output:

    List:
    0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 

    Map.values():
    35 36 33 34 39 37 38 43 42 41 40 22 23 24 25 26 27 28 29 3 2 1 0 7 30 6 5 32 4 31 9 8 19 17 18 15 16 13 14 11 12 21 20 99 98 97 96 95 94 93 92 91 90 10 88 89 79 78 77 82 83 80 81 86 87 84 85 67 66 69 68 70 71 72 73 74 75 76 59 58 57 56 55 64 65 62 63 60 61 49 48 45 44 47 46 51 52 53 54 50 

    Map.keys():
    35 36 33 34 39 37 38 43 42 41 40 22 23 24 25 26 27 28 29 3 2 1 0 7 30 6 5 32 4 31 9 8 19 17 18 15 16 13 14 11 12 21 20 99 98 97 96 95 94 93 92 91 90 10 88 89 79 78 77 82 83 80 81 86 87 84 85 67 66 69 68 70 71 72 73 74 75 76 59 58 57 56 55 64 65 62 63 60 61 49 48 45 44 47 46 51 52 53 54 50 

    Set:
    35 36 33 34 39 37 38 43 42 41 40 22 23 24 25 26 27 28 29 3 2 1 0 7 30 6 5 32 4 31 9 8 19 17 18 15 16 13 14 11 12 21 20 99 98 97 96 95 94 93 92 91 90 10 88 89 79 78 77 82 83 80 81 86 87 84 85 67 66 69 68 70 71 72 73 74 75 76 59 58 57 56 55 64 65 62 63 60 61 49 48 45 44 47 46 51 52 53 54 50

After I applied the updated, it was corrected:

List:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 

Map.values():
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 

Map.keys():
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 

Set:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
  • Did you test whether the data type makes any difference in the behavior? (String vs Integer vs Apex Class vs sObject, etc.) – Mark Pond Apr 7 '15 at 18:34
  • What happens if you .add or .put a value that's already in the collection? – Jeremy Nottingham Apr 7 '15 at 18:40
0

Having wasted hours on debugging some code that cached records in a map I know for a fact that unlike lists, maps don't follow a deterministic order. For instance if you put A followed by B into the map, you may (or may not) get the keys/values in the same order within a for-loop.

It sounds like this update is going to fix that.

  • In the end, Map and Set will still be considered as far as I understand this feature. You should always use List if you want a meaningful ordering. – jpmonette Apr 1 '15 at 7:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.