Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Putting objects of inconsistent type in initialized Map - expected and legal?

Today, I discovered that one is able to put an object that in an existing Map even if the object cannot be cast to the right type.

First, let me start with a simple example:

Map<Integer, String> myMap = new HashMap<>(); //plain old hashmap
myMap.put(9,"star"); //no problem

myMap.put(10, 1.2); //Incompatible type, the compiler yells
Map<Integer, Double> aMap = (Map<Integer, Double>) myMap; //Cannot cast, the compiler yells

So far, everything is expected, as you should not be able to put an object of inconsistent type into an already constructed Map. Now let's consider this:

public class NoRulesForMe {

    static Object theRing;

    public static void main(String[] args){

        Map<Integer, String> myMap = new HashMap<>();
        myMap.put(9,"star");

        Map<Integer, Double> myMapMorphed = castWildly(myMap);
        myMapMorphed.put(99, 3.14);

        System.out.println(myMapMorphed.get(9)); //"star", as we put in
        System.out.println(myMapMorphed.get(99)); //3.14, as we put in
    }

    public static <T> T castWildly(Object value){
        theRing = value;
        T morphed = (T) theRing;
        return morphed;
    }
}

I'm surprised that this didn't cause a run-time error - how does Map achieve this, and is this behavior specified in the JLS or API and thus can be depended upon?

Reason I ask is that I saw a (more involved) version of this in production code, and I wonder, even if this may be befuddling and smelly, can it be guaranteed to work functionality-wise. Any input would be appreciated.

like image 401
flow2k Avatar asked Oct 29 '22 00:10

flow2k


1 Answers

This type of coding is very risky!! Although it will compile, you will notice that the compiler complains with a warning:

Note: NoRulesForMe.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

These warnings, especially since you are using generics, should never be ignored nor just suppressed. You must absolutely be sure (logically following the code) that the cast is safe and won't be causing some problems later on. It is best to always code in such a way that errors are discovered and picked up at compiler time instead of run time. The warning given by the compiler here is telling you that things may go wrong.

You are passing your myMap as an Object to the method castWildly, and when you are casting you are casting from Object to a Map.

The compiler can infer that T in your code has type target of Map<String, Double>, and thus can infer this. However, when casting, it has no information about what (sub)type the Object value (or Object theRing) is. So it has no way in checking that the cast is safe (specifically type safe).

The problem with this code comes in when you retrieve values out of your map. The following code has one extra added line, and the code compiles (with the same warning as above). This is because when retrieving a value as a Double out of a map that is declared as Map<String, Double> is absolutely valid when the compiler does type checking... at run time however, your code will crash (run time crash error shown below). This is very dangerous way to code, especially in production code. You'd rather have your compiler give you errors than deploying production code that compiles and have your product crash when live.

public class NoRulesForMe {

    static Object theRing;

    public static void main(String[] args){

        Map<Integer, String> myMap = new HashMap<>();
        myMap.put(9,"star");

        Map<Integer, Double> myMapMorphed = castWildly(myMap);
        myMapMorphed.put(99, 3.14);

        System.out.println(myMapMorphed.get(9)); //"star", as we put in
        System.out.println(myMapMorphed.get(99)); //3.14, as we put in

        // added to show why this style of coding causes problems
        Double testValue1 = myMapMorphed.get(9);
    }

    public static <T> T castWildly(Object value){
        theRing = value;
        T morphed = (T) theRing;
        return morphed;
    }
}

Run time error when running the above code:

star
3.14
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Double at NoRulesForMe.main(NoRulesForMe.java:19)

For further information, read Effective Java, by Joshua Bloch; Item 24: Eliminate unchecked warnings. (This item is under the heading Generics).

like image 126
Smiley Avatar answered Nov 09 '22 15:11

Smiley