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?
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>();
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;
).
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..?
Commented
Jul 20, 2021 at 10:01
All classes are child of Object class so:
Map<Object,Object> m = New Map<String,String>();
Thanks