【问题标题】:Java Generic Singleton Factory PatternJava 通用单例工厂模式
【发布时间】:2020-11-10 03:34:24
【问题描述】:

我很难理解下面的难题。这是一个可以编译但抛出异常的代码 sn-p

    Exception in thread "main" java.lang.ClassCastException: 
    TestGenericSingleton$$Lambda$1/303563356 cannot be cast to 
    TestGenericSingleton$IntegerConsumer
        at TestGenericSingleton.main(TestGenericSingleton.java:23)
import java.util.function.Consumer;

public class TestGenericSingleton 
{
    static final Consumer<Object> NOOP_SINGLETON = t -> {System.out.println("NOOP Consumer accepting " + t);};
    
    @SuppressWarnings("unchecked")
    static <R extends Consumer<T>, T> R noopConsumer() 
    {
        return (R)NOOP_SINGLETON;
    }
    
    static interface IntegerConsumer extends Consumer<Integer> {};
    
    public static void main(String[] argv) 
    {
        Consumer<Boolean> cb = noopConsumer();
        cb.accept(true);
        
        IntegerConsumer ic = t -> {System.out.println("NOOP Consumer2 accepting " + t);} ;
        ic.accept(3);
        
        ic = noopConsumer();
        ic.accept(3);
        System.out.println("Done");
    }
}

让我难过的是,Java 编译器可以从第 20 行的 lambda 生成一个适当的 IntegerConsumer 兼容对象,但不能使用之前在第 8 行构造为单例的非泛型 lambda。是不是因为第 20 行的 lambda 具有 Consumer 的可具体化子类型,它立即适合 IntegerConsumer 引用的类型,而第 10 行强制转换的 lambda 不能在运行时强制转换为真正的 Consumer 子类型?但是第 8 行的通用有界类型声明不应该处理这个问题吗?非常感谢任何帮助!

【问题讨论】:

  • 因为你有一个演员,编译器不能完成它的工作。它知道第 20 行生成的内部类需要实现 IntegerConsumer,所以它实现了。它无法在第 5 行生成内部类。但也许我误解了你的问题?
  • 我认为原因很明显。 Comsumer 不是 Comsumer 的子类型,当然不能强制转换。

标签: java generics singleton


【解决方案1】:

之前在第 8 行构造为单例的非泛型 lambda 不能使用

我们将移除 lambda 并了解异常的根本原因。让我们考虑以下更简单的示例。

public class TestGenericObject {

    static final Object NOOP_SINGLETON = new Object();

    static <R extends Object> R noopConsumer() {
        return (R) NOOP_SINGLETON;
    }

    public static void main(String[] argv) {
        Object cb = noopConsumer();
        Integer ic = noopConsumer(); // Throws java.lang.ClassCastException: java.lang.Object cannot be cast to java.lang.Integer
    }
}

例外是合理的。 NOOP_SINGLETON 的实际类型是 Object,但我们正在尝试将其转换为 Integer。这与尝试Integer ic = (Integer) new Object() 相同。在您的情况下,出于同样的原因,您不能将 Consumer&lt;Object&gt; 类型转换为 IntegerConsumer


一个有趣的观察是异常不会在noopConsumer() 中引发,而是在main() 中引发。
下面是javap -v -c 方法noopConsumer 的输出

... // Removed lines for clarity
 static <R extends java.lang.Object> R noopConsumer();
    ...
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #2                  // Field NOOP_SINGLETON:Ljava/lang/Object;
         3: areturn

您可以看到没有用于投射的操作码。但是对于main()

  public static void main(java.lang.String[]);
    Code:
      stack=1, locals=3, args_size=1
         0: invokestatic  #3                  // Method noopConsumer:()Ljava/lang/Object;
         3: astore_1
         4: invokestatic  #3                  // Method noopConsumer:()Ljava/lang/Object;
         7: checkcast     #4                  // class java/lang/Integer
        10: astore_2
        11: return

在这一行7:checkcast #4,它检查返回的类型是否为整数。这种行为有两个原因

  1. noopConsumer() 中,R 的更严格界限是 Object,NOOP_SINGLETON 也是 Object 类型。因此,在类型擦除之后,强制转换是多余的并被删除。
  2. main() 进行转换检查的原因又是由于Type Erasure。如链接中所述,如果需要,将在类型擦除期间插入类型转换。

回到 Lambda。 Lambda 使用 invokedynamic 操作码在运行时生成代码。 Thisthis 是很好的资源,可以更好地了解运行时的 lambda 处理。 对于下面的代码,

public static void main(String[] argv) {
        Consumer<Object> NOOP_SINGLETON = t -> {System.out.println("NOOP Consumer accepting " + t);};
        TestGenericSingleton.IntegerConsumer ic = t -> {System.out.println("NOOP Consumer2 accepting " + t);} ;
    }

让我们分析字节码。

public static void main(java.lang.String[]);
   ...
    Code:
      stack=1, locals=3, args_size=1
         0: invokedynamic #2,  0              // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
         5: astore_1
         6: invokedynamic #3,  0              // InvokeDynamic #1:accept:()Lcom/TestGenericSingleton$IntegerConsumer;
        11: astore_2
        12: return

invokedynamic 将 2 个不同的预期类型 Ljava/util/function/Consumer()Lcom/TestGenericSingleton$IntegerConsumer 传递给 LambdaMetafactory.metafactory()
因此,尽管 lambda 中的代码 t -&gt; {System.out.println("NOOP Consumer accepting " + t);} 相同,但它们是两种不同的类型。


总结,lambda 是在运行时构建的,返回的实例将具有声明中指定的类型。因此,NOOP_SINGLETON 的类型为 Consumer,而 ic 的类型为 IntegerConsumer。从Consumer 类型转换为IntegerConsumer 将因同样的原因而失败,Integer ic = (Integer)new Object() 失败。

【讨论】:

  • 非常感谢您的精彩解释!非常感谢您花时间回答这个问题。
  • @AlexanderShenker,干杯。很高兴在 SO 中回答了您的第一个问题
猜你喜欢
  • 2012-03-08
  • 1970-01-01
  • 1970-01-01
  • 2011-01-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多