fireandfuel fireandfuel - 10 days ago 5
Java Question

Broken cast on compiling generic method with JDK 8

I have some legacy code with class

Box
to put and get
Serializable
data into a
Map
, which runs fine on
Oracle JRE 1.8 Update 102
when compiled with
Oracle JDK 1.7 Update 80
. But it don't run properly when I compile it with
Oracle JDK 1.8 Updater 102
. I had some problems with a generic
get
function.

A SSCCE which outputs a formatted date from a
Box
instance using a problematic generic
get
function:

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;

public class Box implements Serializable{

private HashMap<String, Serializable> values = new HashMap<String, Serializable>();

public <T extends Serializable> T get(String key){

return (T) this.values.get(key);
}

public void put(String key,
Serializable value){

this.values.put(key,
value);
}

public static void main(String[] args){

Box box = new Box();
box.put("key",
new Date());

System.out.println(String.format("%1$td.%1$tm.%1$tY",
box.get("key")));
}
}


I get the following exception when it is compiled with JDK 1.8 and I run it with JRE 1.8:


Exception in thread "main" java.lang.ClassCastException: java.util.Date cannot be cast to [Ljava.lang.Object;
at Box.main(Box.java:31)


Some Methods like System.out.println produces a compiler error when used with the
get
function


error: reference to println is ambiguous


while other function runs fine with the
get
function.

The compiler prints out a warning about
unchecked or unsafe operations
and I noticed the main method is compiled to different byte code:

Compiled with 1.7:

public static void main(java.lang.String[]);
Code:
0: new #8 // class Box
3: dup
4: invokespecial #9 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #10 // String key
11: new #11 // class java/util/Date
14: dup
15: invokespecial #12 // Method java/util/Date."<init>":()V
18: invokevirtual #13 // Method put:(Ljava/lang/String;Ljava/io/Serializable;)V
21: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
24: ldc #15 // String %1$td.%1$tm.%1$tY
26: iconst_1
27: anewarray #16 // class java/lang/Object
30: dup
31: iconst_0
32: aload_1
33: ldc #10 // String key
35: invokevirtual #17 // Method get:(Ljava/lang/String;)Ljava/io/Serializable;
38: aastore
39: invokestatic #18 // Method java/lang/String.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
42: invokevirtual #19 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
45: return


Compiled with 1.8:

public static void main(java.lang.String[]);
Code:
0: new #8 // class Box
3: dup
4: invokespecial #9 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #10 // String key
11: new #11 // class java/util/Date
14: dup
15: invokespecial #12 // Method java/util/Date."<init>":()V
18: invokevirtual #13 // Method put:(Ljava/lang/String;Ljava/io/Serializable;)V
21: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
24: ldc #15 // String %1$td.%1$tm.%1$tY
26: aload_1
27: ldc #10 // String key
29: invokevirtual #16 // Method get:(Ljava/lang/String;)Ljava/io/Serializable;
32: checkcast #17 // class "[Ljava/lang/Object;"
35: invokestatic #18 // Method java/lang/String.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
38: invokevirtual #19 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
41: return


Can somebody explain why it is compiled differently?

PS: I already fixed it by giving
Class<T> clazz
as additional parameter to the
get
function.

Answer

Your method

public <T extends Serializable> T get(String key){

  return (T) this.values.get(key);
}

is fundamentally broken as it basically says “whatever the caller wishes, I will return it, as long as it is assignable to Serializable”.

Interestingly, we have similar broken methods every few weeks here, the last one just yesterday.

The key point is, if your method promises to return whatever the caller wishes, I could write:

Date date=box.get("key");

but also

String str=box.get("key");
String[] obj=box.get("key");

As all these types, Date, String, or String[] are assignable to Serializable. Less intuitively, you can even write

Object[] obj=box.get("key");

despite Object[] is not Serializable, because there could be a subtype of Object[] that is Serializable. So the compiler will infer Object[] & Serializable for T (see also here).


The difference between Java 7 and Java 8 is that the Java 7 compiler did not perform this type inference when you put this method invocation as an argument to another invocation (aka “nested method call”). It always used the bounds of the type parameter, i.e. Serializable and found that it has to perform a varargs invocation.

In contrast, Java 8 considers all possibilities. It can infer a non-array type and perform a varargs invocation, but it can also infer an array type and pass it directly to the method String.format(String,Object[]). The rules are simple, a non-vararg invocation is always preferred.

The fix is simple. Don’t make promises you can’t hold.

public Serializable get(String key) {
   return this.values.get(key);
}

and let the caller do the type cast explicitly.

Date date=(Date)box.get("key");

or no cast when an arbitrary object is needed:

System.out.println(String.format("%1$td.%1$tm.%1$tY", box.get("key")));

which is by the way a convoluted variant of

System.out.printf("%1$td.%1$tm.%1$tY%n", box.get("key"));

Alternatively, you can use a Class object to specify the expected type:

public <T extends Serializable> T get(String key, Class<T> type) {
   return type.cast(this.values.get(key));
}

Date date=box.get("key", Date.class);

By the way, referring to Serializable explicitly has no real benefit. There are plenty of place, where serializable objects are returned, see Collections.emptyList(), for example, without declaring Serializable. Consequently, the JRE classes never refer to Serializable this way either. Most notably, not even ObjectOutputStream.writeObject(…) refers to Serializable in its signature, but just accepts Object.

Comments