12

given: Map<String, String> m = new Map<String, String>();

m instanceof Map<String, Object> is always true

then why is m instanceof Map<Object, Object> always false?

19

TL;DR Pretty much the entire "Type" system that governs Maps, Sets, and Lists is broken. Do not trust instanceOf, use your own logical assessment to determine if something is safe or not.


The type system that governs the system usually works pretty well, but it has a flaw when it comes to collections and how they are determined to be the right "size" or not. In fact, it's pretty much broken.

Let's start with your example. Since m instanceOf Map<String, Object> is true, that means that the following code compiles:

// Map<String, String> is an instanceOf Map<String, Object>
Map<String, Object> m = new Map<String, String>();

However, if you try to use it the wrong way...

m.put('Hello', 5);

This code throws a TypeException, because 5 is not a String. In other words, the compiler's type checks are inverted; it allowed an assignment that was factually incorrect, because not all Object values are String values. Note that this is a runtime exception: the compiler failed to notice that you were doing something that was wrong.

It's broken in other ways, too. We already know that a String is an type of Object:

String s = 'Hello World';
// Compile error:
// Operation instanceof is always true since an instance of 
// String is always an instance of Object
System.debug(s instanceOf Object); 

That means that Object can store every type of String. Right? Not according to the compiler:

// Does not compile
Map<String, String> m = new Map<String, Object>();

Wait, what? We can't store a String in an Object value? That can't be right?!

System.debug(new Map<String, Object>() instanceOf Map<String, String>);
// USER_DEBUG|[1]|DEBUG|false

So, we can try to store an Object in a String, but not a String in an Object. It's completely backwards. You can try this with other types, too:

System.debug(new Map<String, Date>() instanceOf Map<String, DateTime>);
System.debug(new Map<String, Integer>() instanceOf Map<String, Decimal>);
System.debug(new Map<String, Integer>() instanceOf Map<String, Long>);

All of these statements fail to compile with "X always instanceOf y" messages. Yet clearly that's not true. If you try to store any of the types on the left into a variable with the data type on the right, it will let you, but crash the moment you try and store a value outside the legal range of the object actually referenced in the code.

There's a whole class of really cool things we can't do because of this inverted type check, and there's plenty of opportunities for errors. I strongly suggest you avoid trusting instanceOf, and instead rely on your own logic to work through if a type is safe or not.

Also, you'll notice that we can't even cast Sets around. For example, neither of the following lines compile:

Set<String> s1 = new Set<Object>();
Set<Object> s2 = new Set<String>();

Finally, this bug also extends to custom classes/inheritance:

class c1 {

}
class c2 extends c1 {

}
// All C2s are C1s, but this won't compile
Map<String, c2> m1 = new Map<String, c1>();
// Not all C2s are C1s, but this compiles and can crash.
Map<String, c1> m2 = new Map<String, c2>();
4
  • I may be misreading or misunderstanding this, but I always thought that Map<String, Object> m = new Map<String, String>(); works because Map<String, Object> can hold anything that Map<String, String> can hold - the same is not true the other way round, hence the reverse would (correctly) fail to compile. This allows you to write generic methods. It's upsetting that the keys can't be cast - I always assumed because of the way the hashing works, which would explain why Set doesn't cast either. "Fixing" that would be very useful. Jul 20 at 5:34
  • @RobBaillie Yes, you've missed the point. We're creating an object that can hold Map<String, String>, and then putting it into a variable that claims it can hold Map<String, Object>. However, we can't do String value = 5;, nor can we do m.put('value',5); Ideally, each object should be able to be "widened" (e.g. we can say Object value = 'Hello';), but not "narrowed" (Object value = 5; String value2 = (String)value;).
    – sfdcfox
    Jul 20 at 5:49
  • @RobBaillie To be fair, I just found out that Java doesn't like casting Maps (it's been a while since I did any serious Java work, so I'd forgotten this wasn't a feature), either, so it's great that they added the functionality, but because it's broken, it's practically as useless as it is in Java.
    – sfdcfox
    Jul 20 at 6:11
  • I think I get what you mean - I was thinking of it as similar to defining a variable as an Object and then assigning a String to it - but your point is that the Map<String,String> does not implement the full interface that is required of Map<String,Object> - however String DOES implement the full interface required of Object. Fair point - and very subtle. Maybe it shouldn't allow casting of Maps at all..? Jul 20 at 10:01
-1

All classes are child of Object class so:

 Map<Object,Object> m = New Map<String,String>();
  1. In above code, m is the variable of map of type object in which map of string type instance is assigned.
  2. So it is always instance of string type map while it is assigned in variable of type map of Object.

Thanks

1
  • SFDCGOD thanks for your reply, but i think you misread my question. I understand the String is an Object. Given that is true, why is an instance of Map<String, String> NOT and instance of Map<Object, Object>. Please refer to my example at the beginning of this thread.
    – davec
    Jan 21 '16 at 1:37

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.