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
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> </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<Id, Contact>, 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<Id, Contact>, 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> </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<String, Contact>, 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<String, Contact>, 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<WrapperClass, Contact>, 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>
PageBlockTable
to the orderedSet
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.