使用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");
作为所有这些类型, Date
, String
或String[]
可分配给Serializable
。 不那么直观,你甚至可以写
Object[] obj=box.get("key");
尽管Object[]
不是Serializable
,因为可能存在可Serializable
的Object[]
子类型。 因此编译器将推断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