使用JDK 8编译generics方法时损坏

我有一些带有类Box遗留代码,可以将Serializable数据放入Map ,当使用Oracle JDK 1.7 Update 80编译时,它可以在Oracle JRE 1.8 Update 102上正常运行。 但是当我使用Oracle JDK 1.8 Updater 102编译它时,它无法正常运行。 我遇到了genericsget函数的一些问题。

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

 import java.io.Serializable; import java.util.Date; import java.util.HashMap; public class Box implements Serializable{ private HashMap values = new HashMap(); public  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"))); } } 

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

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

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

错误:对println的引用不明确

而其他function与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 "":()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."":()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 

用1.8编译:

  public static void main(java.lang.String[]); Code: 0: new #8 // class Box 3: dup 4: invokespecial #9 // Method "":()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."":()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 

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

PS:我已经通过将Class clazz作为get函数的附加参数来修复它。

你的方法

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

从根本上打破,因为它基本上说“无论调用者希望什么,我会返回它,只要它可以分配给Serializable ”。

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

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

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

但也

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

作为所有这些类型, DateStringString[]可分配给Serializable 。 不那么直观,你甚至可以写

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

尽管Object[]不是Serializable ,因为可能存在可SerializableObject[]子类型。 因此编译器将推断T Object[] & Serializable (另见此处 )。


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); } 

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

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

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

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

这是一个复杂的变体

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

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

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

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

顺便说一下,明确提到Serializable并没有什么好处。 有很多地方可以返回可序列化的对象,例如,请参阅Collections.emptyList() ,而不声明Serializable 。 因此,JRE类也不会以这种方式引用Serializable 。 最值得注意的是,甚至ObjectOutputStream.writeObject(…)没有在其签名中引用Serializable ,而只是接受Object

除非您计划将此代码存储为对象的混合,否则我建议使该类具有通用性:

 import java.io.Serializable; import java.util.Date; import java.util.HashMap; public class Box implements Serializable { private HashMap 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 box = new Box<>(); box.put("key", new Date()); System.out.println(String.format("%1$td.%1$tm.%1$tY", box.get("key"))); } } 

完成后,我会抛弃Box并使用HashMap