ClassCastException in Java 11 but not in Java 8 when using HashMap?
Asked Answered
J

2

12

Please take a look at my code:

Object longL = 2548214;
Map<String, Object> map = new HashMap<String, Object>(1);
map.put("LongNumber", longL);
List<Map<String, Object>> returnlist = new ArrayList(10);
returnlist.add(map);

List<Object> versionMap1 = new ArrayList(10);
versionMap1.add(returnlist);

List<Map<String, String>> docIdVersionNameMap = new ArrayList<>();
docIdVersionNameMap.addAll((List<Map<String, String>>)versionMap1.get(0));

Map<String, String> versionDoc=docIdVersionNameMap.get(0);

Map<String,String> versionDocInfo=new HashMap<String,String>(1);
versionDocInfo.put(versionDoc.get("LongNumber"),"abc");
System.out.println(versionDocInfo.toString());

In Java_1.8_60 (Compile & Run) this code is running fine, but when compiled and run in Java 11 it is throwing the following exception:

Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of l
oader 'bootstrap')
        at teststringandlong.Trial.main(Trial.java:35)

Are there any changes in Java 11 regarding HashMap?

Joline answered 17/3, 2019 at 7:56 Comment(5)
I'm getting the same error in Java 8, and it's not surprising - you are trying to put an Integer as a key in a Map<String,String>.Greenquist
Java 1.8_60 is pretty old but still hard to believe it is running correctly. With a bit newer version I got java version "1.8.0_112" Java(TM) SE Runtime Environment (build 1.8.0_112-b15) ... Exception in thread "main" java.lang.ClassCastException: ...Fitly
@YassinHajaj as key: versionDocInfo.put(versionDoc.get("LongNumber"),"abc");Greenquist
@Eran, you're right indeed. What's strange is that the same exact code works on ideone.com [HotSpot 8u112] and JDoodle [JDK 10.0.1]. ideone.com/296BMB & jdoodle.com/online-java-compiler (just copy past it with correct imports)Garnett
I don't get the close votes. Please don't close it, this code works on multiple platforms, and on my PC locally too.Garnett
S
19

The ClassCastException being thrown is correct. Not having it thrown was caused by a bug in javac, which was fixed in JDK 9 by JDK-8058199. Your code is technically relying on heap pollution not being picked up, so it was never guaranteed to not break.

Basically, in Java 11 (but starting from 9), an extra cast is inserted after getting the value for "LongNumber" from the map on the second to last line. This:

versionDocInfo.put(versionDoc.get("LongNumber"),"abc");

Is compiled as:

versionDocInfo.put((String) versionDoc.get("LongNumber"),"abc");

When compiling your code with javac 1.8.0_162, the bytecode for the second to last line is:

 114: aload         7
 116: aload         6
 118: ldc           #6                  // String LongNumber
 120: invokeinterface #16,  2           // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
 125: ldc           #17                 // String abc
 127: invokeinterface #7,  3            // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;

Notice that there is no checkcast instruction after 120:. However, when using javac 9.0.4:

 114: aload         7
 116: aload         6
 118: ldc           #6                  // String LongNumber
 120: invokeinterface #16,  2           // InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object;
 125: checkcast     #17                 // class java/lang/String
 128: ldc           #18                 // String abc
 130: invokeinterface #7,  3            // InterfaceMethod java/util/Map.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;

Notice that there is a checkcast instruction at 125:.

This instruction makes the difference, as it basically does an extra type check after getting the value from the versionDoc map. Basically doing this:

versionDocInfo.put((String) versionDoc.get("LongNumber"),"abc");

In Java 11 (starting from 9).


As noted in the comments; the type of the value for "LongNumber" is Integer, which is inside a Map<String, String> due to the unchecked cast a few lines earlier:

docIdVersionNameMap.addAll((List<Map<String, String>>) versionMap1.get(0));

Where you indirectly cast a Map<String, Object> to a Map<String, String>, even though one of the values is an Integer. The difference is only that there's an extra cast to check the type after getting the value from the map.

Note that the missing checkcast was a bug in javac, so compiling with a different compiler, or different versions of javac could result in different behavior.

Saloop answered 17/3, 2019 at 12:57 Comment(4)
How can we explain that it runs on Java 10 on jdoodle.com and also that some of us have the code failing on Java 8 and some not?Garnett
@YassinHajaj Well, it's really javac that's at fault, not java, so if you're using e.g. Eclipse to compile the class you might get different behavior. If you can, you should check the byte code with javap -c ...Saloop
@YassinHajaj I'm not sure about jdoodle.com, using the same version of Java 10 I am getting the CCE being thrown. Must be something about their setup... Testing with e.g. var x = 10 this does not compile on jdoodle.com, so I guess they're using javac from JDK 8 to compile, and then running with java 10.Saloop
Alright thanks, checkin the bytecode locally, it's indeed not invoking the checkcast operation ! Thanks, this is very interesting :)Garnett
A
0

Thanks for the sample!! I nearly smashed my head upon the keyboard trying to reproduce the same thing in a test project after a PROD bug - the latter was 100% reproducible, but involved a lot of code around it, and no matter what I tried, the sample project didn't work same way... until I found your question :) So for those looking for the same problem, here is even more simplified sample which breaks with JDK 9+ but works on Java 1.8.0_372:

Object longL = 2548214L;

Map<String, Object> sourceMap = new HashMap<>(1);
sourceMap.put("LongNumber", longL);

Map<String, String> wrongTypedMap = (Map<String, String>)(Map)sourceMap;

Map<String, String> targetMap = new HashMap<>(1);
targetMap.put(wrongTypedMap.get("LongNumber"), "abc");

System.out.println(targetMap);

So it's interesting to notice that this bug is not that easy to reproduce - i.e. to make a code like that which is working differently on Java 8 vs. 9+ - here it involves a few layers where the type erasure finally makes it to the bug on Java 8, but being caught on Java 9+

Aksum answered 14/5, 2024 at 2:57 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.