5

I've noticed some strange(?) behaviour with Lists and Maps. Below is a snippet of code:

// Query for a List of Accounts
List<Account> accountList = [SELECT Id, BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry FROM Account];

// Create 2 empty maps. One for old versions of records, one for new.
Map<Id, Account> oldAccMap = new Map<Id, Account>();
Map<Id, Account> newAccMap = new Map<Id, Account>();

// Populate the old map with the initial value of the records
oldAccMap.putAll(accountList );

// Manipulate the List
for (Account acc : accountList) {
    acc.BillingStreet = '1-13 St Giles High St';
    acc.BillingCity = 'London';
    acc.BillingState = '';
    acc.BillingPostalCode = 'WC2H 8LG';
    acc.BillingCountry = 'United Kingdom';
}

// Populate the new map with the new value of the records
newAccMap.putAll(accountList );

So, effectively I'm querying for records, populating a Map with them, changing those records and adding them to a different map. In effect I'm trying to do what a Trigger does with new and old records.

This doesn't work though because I'm finding that the values in new and old are mirroring eachother despite being added to one list, changed and added to another List. I'm expected the Lists to have the same AccountIds, just different values.

The only way I've found to fix this is by making 2 queries.

List<Account> newAccountList = [SELECT Id, BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry FROM Account];
List<Account> oldAccountList = [SELECT Id, BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry FROM Account];
Map<Id, Account> oldAccMap = new Map<Id, Account>();
Map<Id, Account> newAccMap = new Map<Id, Account>();

oldAccMap.putAll(oldAccountList);

for (Account acc : newAccountList) {
    acc.BillingStreet = '1-13 St Giles High St';
    acc.BillingCity = 'London';
    acc.BillingState = '';
    acc.BillingPostalCode = 'WC2H 8LG';
    acc.BillingCountry = 'United Kingdom';
}

newAccMap.putAll(newAccountList);

I'd have thought querying for records, adding them to a Map, changing the List, adding them to a different Map would result in two different values when the old and new maps are compared. Instead they're the same.

I hope I'm making sense.

So my question is, is this behaviour what's to be expected? It seems odd to me.

2 Answers 2

8

When you add a record to a collection (or, in your case, use one collection to build another), what you're doing is making references to the original records.

Because they are references, when you change a field value on one reference, you end up changing the field value on all of them.

To 'break' the references so that your old and new maps are independent of each other, you'll need to clone the records (which creates a completely separate instance of the record(s), in memory).

The following will work as you are expecting:

// Query for a List of Accounts
List<Account> accountList = [SELECT Id, BillingStreet, BillingCity, BillingState, BillingPostalCode, BillingCountry FROM Account];

// Create 2 maps. One for old versions of records, one for new.
// I didn't see any reason to avoid populating old map at initialization, so that's
//   what I'm doing.
Map<Id, Account> oldAccMap = new Map<Id, Account>(accountList);
// Instead of initializing the new map with an empty map, let's deepClone() the old map.
// This will create clones of all of the records held in the old map
Map<Id, Account> newAccMap = oldAccMap.deepClone();

// Now you're free to change field values on records in one map, and it will not
//   affect the corresponding record in the other map.

+edit:

Now that I'm fully awake, and took a second look at this, I suppose I should elaborate a little more. Cloning isn't the only way to make a new in-memory instance of an object (or collection of objects). As you discovered yourself, each query returns a new, independent collection of records. Using the new keyword (a la new Account();) also creates a new in-memory instance of an object.

In this situation, cloning happens to be the easiest (and fastest) way to accomplish what you're looking to do (it also saves a precious query, which makes it an automatic 'yes' for me in pretty much every case).

One thing to look out for is that maintaining multiple instances of an object in memory takes up more heap space than one instance + many references do. There is a governor limit on heap usage, but I myself have never run into it. If you run into heap issues, but need independent instances, I think the only solution would be to split things into separate transactions (batch/queueable).

2
  • Makes complete sense. Thank you. Here's me thinking I uncovered a bug or something! ¯_(ツ)_/¯
    – Dan Jones
    Commented Sep 11, 2017 at 12:48
  • Thanks for the addition Derek. I was suspicious about it referencing the same List, hence why I queries a second time. Admitted I'd have probably overlooked cloning so I do appreciate refreshing my knowledge on that one. I'm trying to be really governor limit conscious as this is comparing addresses to know whether or not we should make a callout for geolocation. Really good answer and appreciate the clarification.
    – Dan Jones
    Commented Sep 11, 2017 at 16:08
0

Wrong Solution: Deep Cloning of Map

I tried deep cloning of map but I still see same records in both map. This is not the correct solution to do deep cloning of map.

// Query for a List of Accounts
List<Account> accountList = [SELECT Id, BillingStreet, BillingCity, BillingState, 
                             BillingPostalCode, BillingCountry FROM Account];

Map<Id, Account> oldAccMap = new Map<Id, Account>(accountList);
Map<Id, Account> newAccMap = oldAccMap.deepClone();

for (Account acc : accountList) {
    acc.BillingStreet = '1-13 St Giles High St';
    acc.BillingCity = 'London';
    acc.BillingState = '';
    acc.BillingPostalCode = 'WC2H 8LG';
    acc.BillingCountry = 'United Kingdom';
}

newAccMap.putAll(accountList);
system.debug('oldAccMap: '+ oldAccMap);
system.debug('newAccMap: '+ newAccMap);
Correct Solution is below. Please let me know if anyone has better solution.

List<Account> accountList = [SELECT Id, BillingStreet, BillingCity, BillingState, 
                             BillingPostalCode, BillingCountry FROM Account limit 5];
List<Account> newList  = new List<Account>();
Map<Id,Account> oldAccMap = new Map<Id, Account>(accountList);
system.debug('oldAccMap1: '+ oldAccMap);
Map<Id,Account> newAccMap = new Map<Id, Account>();

for (Account acc : accountList) {
    Account obj = acc.clone(true);
    obj.BillingStreet = '1-13 St Giles High St';
    obj.BillingCity = 'London';
    obj.BillingState = '';
    obj.BillingPostalCode = 'WC2H 8LG';
    obj.BillingCountry = 'United Kingdom';
    newList.add(obj);
    system.debug('newList: '+ newList);
}

newAccMap.putAll(newList);
system.debug('oldAccMap: '+ oldAccMap);
system.debug('newAccMap: '+ newAccMap);

Correct Solution: Deep Cloning of List

I have cloned the reference of list and stored in another variable--> Account obj = acc.clone(true);

List<Account> accountList = [SELECT Id, BillingStreet, BillingCity, BillingState, 
                             BillingPostalCode, BillingCountry FROM Account limit 5];
List<Account> newList  = new List<Account>();
// Create 2 maps. One for old versions of records, one for new.
Map<Id,Account> oldAccMap = new Map<Id, Account>(accountList);
system.debug('oldAccMap1: '+ oldAccMap);
// Instead of initializing the new map with an empty map, let's deepClone() the old map.
// This will create clones of all of the records held in the old map
Map<Id,Account> newAccMap = new Map<Id, Account>();
//   affect the corresponding record in the other map.

for (Account acc : accountList) {
    Account obj = acc.clone(true);
    obj.BillingStreet = '1-13 St Giles High St';
    obj.BillingCity = 'London';
    obj.BillingState = '';
    obj.BillingPostalCode = 'WC2H 8LG';
    obj.BillingCountry = 'United Kingdom';
    newList.add(obj);
    system.debug('newList: '+ newList);
}

newAccMap.putAll(newList);
system.debug('oldAccMap: '+ oldAccMap);
system.debug('newAccMap: '+ newAccMap);

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .