5

I came across some very strange behaviour when working with a Map with Decimal keys today. It seems like the Decimal type can't handle Integer assignments properly, but allows them nonetheless. I've written some example code demonstrating the problem:

Map<Decimal, String> m = new Map<Decimal, String>();
m.put(1234, 'Integer key'); 
m.put(1234.0, 'Decimal key');
m.put(1234.5, 'Decimal Unique key');

System.debug(m.get(1234));      // => Integer key, why was this not overwritten if 1234 == 1234.0?
System.debug(m.get(1234.0));    // => Decimal key
System.debug(m.get(1234.5));    // => Decimal Unique key

System.debug(m);        //Order of insertion matters, newest value does not override existing one but is not shown on map printout...
for(Decimal d : m.keySet()){
    System.debug(d);    //...unless you print it out yourself, because this loop shows all the keys
}

//Sanity check
System.debug(1234 == 1234.0); //True

//Digging deeper
Decimal fake_d = 1234;
Decimal real_d = 1234.0;
Integer real_i = 1234;
//Integer fake_i = 1234.0; //Illegal assignment, typechecking only works one way, it seems

//More sanity checks
System.debug(fake_d == real_d); //True (Except when used in a Map, apparently)
System.debug(fake_d == real_i); //True

System.debug(m.get(real_i)); // => Integer key
System.debug(m.get(fake_d)); // => Integer key, even with fake_d explicitely declared as a Decimal type, it still pretends to be an Integer.
System.debug(m.get(real_d)); // => Decimal key

I found this behaviour when I tried to fetch a value from a map using a Decimal sObject field, only to find that the keys I used to populate the Map (coming from a different Decimal field) were interpreted as Integers since they had no real decimal digits.

Maybe using Decimals as Map keys is not supported for some reason? Is it expected behaviour for 1234 and 1234.0 being distinct values in this case or is this an oversight in the Map implementation?

7

This appears to be a defect, and I can tell you why. There are two functions that determine a key's behavior: equals and hashCode. The map implementation checks the results from equals and hashCode to see if a value is truly equal. Try the following:

System.assertEquals(System.hashCode(1234), System.hashCode(1234.0));

You'll find that you'll get an exception:

System.AssertException: Assertion Failed: Expected: 1234, Actual: 382541

As you can see, Decimal and Integer return separate hash code values for values that equals returns true for. This causes the system to get confused and create two entries (different keys) for what should be an equal value.

Do not attempt to mix classes when using keys (even primitives), or you'll get crazy results. This includes Id/String, Decimal/Integer/Double/Long, and any other apparently compatible values.

In the meantime, I think we should probably log a defect with salesforce.com, because this is strictly crazy.

0

This is Decimal problem. And it's not a bug. The way you initialise a Decimal defines its scale. And all key types are serialised before being used as keys. Check this out:

Decimal d1 = 1234;
Decimal d2 = 1234.0;
Decimal d3 = 1234.00;
Decimal d4 = 1234.000;
System.debug(d1.scale());
System.debug(d2.scale());
System.debug(d3.scale());
System.debug(d4.scale());    

This prints out 0, 1, 2, 3 respectively. The values of numbers are the same, but actual objects aren't. JSON.serialize(d1) would return 1234 (exactly the same as Integer of value 1234) and JSON.serialize(d4) would return 1234.000 wich means all these variables would be different keys.

I would suggest not to use decimals in maps, but if you have to, use stripTrailingZeros() method on all puts and gets. JSON.serialize(d4.stripTrailingZeros()) would return 1234.

Decimal fake_d = 1234;
Decimal real_d = 1234.0;
System.assertEquals(System.hashCode(fake_d.stripTrailingZeros()), System.hashCode(real_d.stripTrailingZeros())); // all good

Map<Decimal, String> m = new Map<Decimal, String>();
m.put(fake_d.stripTrailingZeros(), 'Integer key'); 
m.put(real_d.stripTrailingZeros(), 'Decimal key');   // key is 1234, not 1234.0

System.debug(m.get(fake_d.stripTrailingZeros()));    // => Decimal key
System.debug(m.get(real_d.stripTrailingZeros()));    // => Decimal key

Also funny thing. Turns out debug without JSON.serialize() doesn't show the whole map in this case.

Map<Decimal, String> m = new Map<Decimal, String>();
m.put(1234, 'Integer key'); 
m.put(1234.0, 'Decimal key');
m.put(1234.5, 'Decimal Unique key');
System.debug(m); // => {1234=Integer key, 1234.5=Decimal Unique key} // misleading output
System.debug(JSON.serialize(m)); 
// => {"1234.5":"Decimal Unique key","1234.0":"Decimal key","1234":"Integer key"}

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.