使用JDK 8编译泛型方法时损坏

fir*_*uel 4 java generics java-8

我有一些遗留代码Box用于将Serializable数据放入并获取数据Map,Oracle JRE 1.8 Update 102在编译时运行正常Oracle JDK 1.7 Update 80.但是当我编译它时,它无法正常运行Oracle JDK 1.8 Updater 102.我在泛型get函数方面遇到了一些问题.

SSCCE Box使用有问题的通用get函数从实例输出格式化日期:

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")));
   }
}
Run Code Online (Sandbox Code Playgroud)

使用JDK 1.8编译时,我得到以下异常,并使用JRE 1.8运行它:

线程"main"中的异常java.lang.ClassCastException:java.util.Date不能转换为[Ljava.lang.Object; 在Box.main(Box.java:31)

get函数一起使用时,某些方法(如System.out.println)会产生编译器错误

错误:对println的引用不明确

而其他功能与该get功能运行良好.

编译器打印出警告unchecked or unsafe operations,我注意到main方法被编译为不同的字节代码:

用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
Run Code Online (Sandbox Code Playgroud)

用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
Run Code Online (Sandbox Code Playgroud)

有人可以解释为什么编译方式不同吗?

PS:我已经通过给函数提供Class<T> clazz附加参数来修复它get.

Hol*_*ger 8

你的方法

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

  return (T) this.values.get(key);
}
Run Code Online (Sandbox Code Playgroud)

从根本上打破,因为它基本上说"无论来电者的意愿如何,只要它可以分配给我,我就会把它归还Serializable".

有趣的是,我们每隔几周就有类似的破碎方法,最后一个就在昨天.

关键是,如果你的方法承诺返回调用者的意愿,我可以写:

Date date=box.get("key");
Run Code Online (Sandbox Code Playgroud)

但是也

String str=box.get("key");
String[] obj=box.get("key");
Run Code Online (Sandbox Code Playgroud)

由于所有这些类型Date,String或者String[]是分配给Serializable.不那么直观,你甚至可以写

Object[] obj=box.get("key");
Run Code Online (Sandbox Code Playgroud)

尽管Object[]不是Serializable,因为有可能是一个亚型Object[]Serializable.所以编译器会推断出Object[] & SerializableT(见这里).


Java 7和Java 8之间的区别在于,当您将此方法调用作为参数添加到另一个调用(也称为"嵌套方法调用")时,Java 7编译器不执行此类型推断.它总是使用类型参数的边界,即Serializable发现它必须执行varargs调用.

相比之下,Java 8考虑了所有可能性.它可以推断非数组类型并执行varargs调用,但它也可以推断数组类型并将其直接传递给方法String.format(String,Object[]).规则很简单,非vararg调用总是首选.

修复很简单.不要做出你无法忍受的承诺.

public Serializable get(String key) {
   return this.values.get(key);
}
Run Code Online (Sandbox Code Playgroud)

并让调用者明确地进行类型转换.

Date date=(Date)box.get("key");
Run Code Online (Sandbox Code Playgroud)

或者在需要任意对象时不进行强制转换:

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

顺便说一下,这是一个错综复杂的变种

System.out.printf("%1$td.%1$tm.%1$tY%n", box.get("key"));
Run Code Online (Sandbox Code Playgroud)

或者,您可以使用Class对象指定期望的类型:

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

...

Date date=box.get("key", Date.class);
Run Code Online (Sandbox Code Playgroud)

顺便说一句,Serializable明确指出没有实际的好处.有很多地方可以返回可序列化的对象Collections.emptyList(),例如,请参阅,而不进行声明Serializable.因此,JRE类也从未引用过Serializable这种方式.最值得注意的是,甚至没有在其签名中ObjectOutputStream.writeObject(…)提及Serializable,而是接受Object.