10

I'm running into some weird behavior when using an Enum as a Key in a map. This same code works fine in other contexts, but not when in batch.

I have an Enum to represent currency types:

   public enum CurrencyType {
       USD, AUD, GBP, CAD
   }    

In an exchange client, I have a Map<CurrencyType, Decimal>. However, when I try to get a value using an Enum, it's returning null.

  private Map<CurrencyType, Decimal> exchangeRates;

   /**
     * Returns the exchange rate for any given currency vs the base
     * @param  currencyType The currency to get the exchange rate for
     * @return              The Exchange Rate for given currency
     */
  public Decimal getExchangeRate(CurrencyType currencyType){
        System.debug(currencyType);  //GBP
        System.debug(exchangeRates); //{USD=1.0, AUD=1.3184, CAD=1.3065, GBP=0.79164}
        System.debug(exchangeRates.containsKey(currencyType)); //false

        for(CurrencyType ct : exchangeRates.keySet()){
            System.debug(ct); //when == GBP
            system.debug(ct == currencyType); //true
            System.debug(ct.hashCode()); //127990640
            System.debug(currencyType.hashCode()); //608648681
        }

        return exchangeRates.get(currencyType); //returns null
   }

UPDATE:

Here is the Raw Debug Statements so you can see everything is populated properly:

12:50:53.0 (19696843)|USER_DEBUG|[55]|DEBUG|~Getting Exchange Rates~
12:50:53.0 (19711834)|USER_DEBUG|[56]|DEBUG|GBP
12:50:53.0 (19752692)|USER_DEBUG|[57]|DEBUG|{USD=1.0, AUD=1.3184, CAD=1.3065, GBP=0.79164}
12:50:53.0 (19786841)|USER_DEBUG|[58]|DEBUG|false
12:50:53.0 (19805826)|USER_DEBUG|[59]|DEBUG|608648681
12:50:53.0 (19815542)|USER_DEBUG|[60]|DEBUG|~Testing vs Keyset~
12:50:53.0 (19879783)|USER_DEBUG|[62]|DEBUG|USD
12:50:53.0 (19912155)|USER_DEBUG|[63]|DEBUG|false
12:50:53.0 (19930846)|USER_DEBUG|[64]|DEBUG|70283834
12:50:53.0 (19945440)|USER_DEBUG|[65]|DEBUG|608648681
12:50:53.0 (19973023)|USER_DEBUG|[62]|DEBUG|AUD
12:50:53.0 (20006415)|USER_DEBUG|[63]|DEBUG|false
12:50:53.0 (20026344)|USER_DEBUG|[64]|DEBUG|1746277182
12:50:53.0 (20041304)|USER_DEBUG|[65]|DEBUG|608648681
12:50:53.0 (20068693)|USER_DEBUG|[62]|DEBUG|CAD
12:50:53.0 (20097827)|USER_DEBUG|[63]|DEBUG|false
12:50:53.0 (20116138)|USER_DEBUG|[64]|DEBUG|76325736
12:50:53.0 (20130603)|USER_DEBUG|[65]|DEBUG|608648681
12:50:53.0 (20157100)|USER_DEBUG|[62]|DEBUG|GBP
12:50:53.0 (20186003)|USER_DEBUG|[63]|DEBUG|true
12:50:53.0 (20204017)|USER_DEBUG|[64]|DEBUG|127990640
12:50:53.0 (20218331)|USER_DEBUG|[65]|DEBUG|608648681

As you can see in the last two lines, the enum equals true when testing directly against the keyset(). However containsKey() returns false (USER_DEBUG|[58]);

Weird thing is... This definitely seems to work in other contexts. I know I could easily work around this by using enum.name() as the key instead of the enum itself, but I really want know whats going on here.

UPDATE 2:

I added hashCode() details and confirmed that they are different! So basically it's possible for two enums of the same value to generate different Hash Codes!?

  • is there any way I can see how your map is formed? – Rao Feb 1 '17 at 19:30
  • @Rao, there's quite a bit of code involved because I'm pulling data from a Currency Exchange API. I think the issue is the context. This same function works as expected when running in a VF page or aynon apex. However, I'm getting this bug when running in a batch context. The Map is initialized in the Batch Start method. I'm going to do some more digging and see if I can replicate with a simpler use case – NSjonas Feb 1 '17 at 19:35
  • Maybe enum is being treated as Object instead. Can you try as passing the key: CurrencyType.GBP ...........also trim the input parameter to make sure that there is no trailing space – Ayub Feb 1 '17 at 19:36
  • @AyubAnsari I assume the op would have done the same, the class would not save unless its a CurrencyType member – Rao Feb 1 '17 at 19:39
  • 1
    @Rao See above... I was able to confirm that the hashcodes between two Identical enums did not match!!! this seems like a platform bug to me – NSjonas Feb 1 '17 at 19:57
12

I think I've figured it out.

In java, the hashCode for an enum is not consistent across different executions.

My Map is being initialized in the Batchable start() (with Database.Stateful). Then I try to access the map in Batchable execute().

Behind the scenes, salesforce must be running start() & execute() in separate java executions. This means that when the hashcode is evaluated in the start() method it is not the same as when it is evaluated in the execute()

In short, Enums cannot be used as keys in when using Database.Stateful.

UPDATE:

I did some testing and it only seems to be an issue when you use a query locator and only fails on the second iterator. I've replicated it with the following code:

global class BatchEnumTesting implements Database.Batchable<SObject>, Database.Stateful {

    public Enum Foo{
        A, B, C
    }


    private Integer i;
    private Map<Foo, String> enumMap;

    global BatchEnumTesting() {}

    global Database.QueryLocator start(Database.BatchableContext BC) {
        i = 1;

        enumMap = new Map<Foo, String>();
        enumMap.put(Foo.A,'Testing1');
        enumMap.put(Foo.B,'Testing2');
        enumMap.put(Foo.C,'Testing3');

        for(Foo e : Foo.values()){
            System.debug('Key: ' + e.Name()  + ' | ' + e.hashCode());
        }

        return Database.getQueryLocator('SELECT Id FROM Account Limit 2');
    }

    global void execute(Database.BatchableContext BC, List<SObject> scope) {
        System.debug(i++);

        for(Foo e : enumMap.keySet()){
            System.debug('Key: ' + e.Name());
            if(e == Foo.A){
                System.debug('Hash Compare: ' + e.hashCode() + ' | ' + Foo.A.hashCode());
                System.debug('Map Match : ' + enumMap.containsKey(Foo.A));
            }else if(e == Foo.B){
                System.debug('Hash Compare: ' + e.hashCode() + ' | ' + Foo.B.hashCode());
                System.debug('Map Match : ' + enumMap.containsKey(Foo.B));
            }if(e == Foo.C){
                System.debug('Hash Compare: ' + e.hashCode() + ' | ' + Foo.C.hashCode());
                System.debug('Map Match : ' + enumMap.containsKey(Foo.C));
            }
        }
    }
}

global void finish(Database.BatchableContext BC) {}

Run with a batch size of 1:

Database.executeBatch(new BatchEnumTesting(), 1);
  • So finally- In short, Enums cannot be used as keys in when using Database.Stateful.....gonna remember that:) – Ayub Feb 2 '17 at 18:15
  • I faced a similar issue with Map(string,object) passing b/w start and execute methods. I was trying to pass an object as a value with a string as a key and it somehow did not like it. So, to make it work, i had to pass the object as String, passing all the fields from that object concatenating them. Maps & Batchables have to be used with caution. – Shalini SF Feb 2 '17 at 22:51
  • @ShaliniMendu very odd... I think it might have something to do with the fact that the state must be serialized to pass between executions. Some objects are not serializable. – NSjonas Feb 2 '17 at 23:07
  • @NSjonas, true! The issue is when you code, you expect these things to work seamlessly and it takes an extra amount of time/analysis/effort to fix these issues. – Shalini SF Feb 3 '17 at 4:28
  • @ShaliniMendu especially since this issue in particular will NEVER be caught by a failing unit test or even smaller manual tests (since it only fails when more than 1 batch is run) – NSjonas Feb 3 '17 at 4:33

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.