简体   繁体   中英

Java Generic Singleton Factory Pattern

I am having a stump of a time understanding the condundrum below. Here is a code snippet that compiles, yet throws the exception

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

What stumps me is that the Java compiler can generate a proper IntegerConsumer-compatible object out of the lambda on line 20, yet the previously constructed non-generic lambda constructed as the singleton on line 8 can not be used. Is that because the lambda on line 20 has a reifiable subtype of the Consumer that fits the type of the IntegerConsumer reference immediately, whereas the lambda that is cast on line 10 can not be cast at runtime to a real subtype of Consumer ? But then shouldn't the generic bounded type declaration on line 8 take care of that? Any help is really appreciated !

the previously constructed non-generic lambda constructed as the singleton on line 8 can not be used

We will remove lambda and understand the root cause of the exception. Let's consider the below simpler example.

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

The exception is justified. NOOP_SINGLETON's actual type is Object but we are trying to cast it to Integer. This would be same as trying, Integer ic = (Integer) new Object() . In your case, for the same reason you cannot cast Consumer<Object> type to IntegerConsumer .


One interesting observation is the exception is not thrown within the noopConsumer() and is rather thrown in the main() .
Below is the output of javap -v -c for the method 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

You can see there is no op code present for casting. But for the 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

at this line 7:checkcast #4 , it checks if returned type is of Integer. This behaviour is due to 2 reasons

  1. In noopConsumer() , the tighter bound of R is Object and NOOP_SINGLETON is of type Object as well. Hence, post type erasure, the casting is redundant and removed.
  2. The reason main() has a cast check is again due to Type Erasure . As mentioned in the link, if required, type casting will be inserted during type erasure.

Back to Lambdas. Lambdas use invokedynamic opcode to generate code at runtime. This and this are excellent resources to understand better about lambda handling at runtime. For the below code,

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

lets analyse the byte code.

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

The invokedynamic passes 2 different expected types Ljava/util/function/Consumer and ()Lcom/TestGenericSingleton$IntegerConsumer to LambdaMetafactory.metafactory() .
So though the code t -> {System.out.println("NOOP Consumer accepting " + t);} within the lambdas are same, they are 2 different types.


To summarize , the lambdas are built at runtime and the returned instance will have the type specified in the declaration. Hence, NOOP_SINGLETON is of type Consumer and ic is of type IntegerConsumer . Casting from type Consumer to IntegerConsumer will fail for the same reason, Integer ic = (Integer)new Object() fails.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM