【问题标题】:Broken cast on compiling generic method with JDK 8使用 JDK 8 编译泛型方法时出现问题
【发布时间】:2017-04-05 21:34:00
【问题描述】:

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

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

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

使用 JDK 1.8 编译并使用 JRE 1.8 运行时出现以下异常:

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

System.out.println 等一些方法在与get 函数一起使用时会产生编译器错误

错误:对 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

用 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

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

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

【问题讨论】:

  • @davida。谢谢。
  • 不管有什么错误,你的通用方法都被破坏了。 T 正是调用者所期望的;你没有理由相信这个转换会成功(因此,未经检查的警告。)类型 var 只出现在返回类型中的泛型方法几乎总是被破坏,并且只提供类型安全的错觉。最好只返回可序列化(或对象)并让调用者进行转换。
  • @BrianGoetz 你在发帖前读过Holger's answer 吗?如果您有什么要补充的,请随时评论此答案。
  • @fireandfuel 当我发表评论时,没有发布任何答案。

标签: java generics java-8


【解决方案1】:

你的方法

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

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

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

有趣的是,我们这里每隔几周就会有类似的破坏方法,the last one just yesterday

关键是,如果您的方法承诺返回调用者希望的任何内容,我可以这样写:

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

还有

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

由于所有这些类型,DateStringString[] 都可以分配给 Serializable。不太直观,你甚至可以写

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

尽管Object[] 不是Serializable,因为Object[] 的子类型可能是Serializable。所以编译器将推断Object[] &amp; SerializableT(另见here)。


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

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

修复很简单。不要做出无法兑现的承诺。

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 extends Serializable> T get(String key, Class<T> 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

【讨论】:

    【解决方案2】:

    除非您打算让此代码存储对象的混合,否则我建议将类设为通用:

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

    完成后,我会放弃 Box 并使用 HashMap&lt;String, ...&gt;

    【讨论】:

    • 这不是问题的正确答案,当然这是解决问题的方法。正如我已经写的那样,它适用于作为参数给出的类。
    • 顺便说一句。 Box 类的实现是为了跨同一项目的多个模块传输数据(对象的混合)。恕我直言,这是一个设计缺陷。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多