Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 695
fireandfuel Avatar asked Jan 06 '23 00:01

fireandfuel


2 Answers

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.

like image 179
Holger Avatar answered Jan 07 '23 13:01

Holger


Unless you're planning to make this code store a mixture of objects, then I would recommend making the class generic:

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

public class Box<T extends Serializable> implements Serializable {

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

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

   public void put(String key,
                   T value){

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

   public static void main(String[] args){

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

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

Having done that, I'd then ditch Box and just use HashMap<String, ...>

like image 40
Ashley Frieze Avatar answered Jan 07 '23 14:01

Ashley Frieze