Why is this type inference not working with this Lambda expression scenario?

后端 未结 6 769
小蘑菇
小蘑菇 2020-12-24 01:23

I have a weird scenario where type inference isn\'t working as I\'d expect when using a lambda expression. Here\'s an approximation of my real scenario:

stat         


        
6条回答
  •  别那么骄傲
    2020-12-24 02:17

    Under the Hood

    Using some hidden javac features, we can get more information about what's happening:

    $ javac -XDverboseResolution=deferred-inference,success,applicable LambdaInference.java 
    LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
        Foo.foo(value -> true).booleanValue(); // Compile error here
           ^
      phase: BASIC
      with actuals: 
      with type-args: no arguments
      candidates:
          #0 applicable method found: foo(Bar)
            (partially instantiated to: (Bar)Object)
      where T is a type-variable:
        T extends Object declared in method foo(Bar)
    LambdaInference.java:16: Note: Deferred instantiation of method foo(Bar)
        Foo.foo(value -> true).booleanValue(); // Compile error here
               ^
      instantiated signature: (Bar)Object
      target-type: 
      where T is a type-variable:
        T extends Object declared in method foo(Bar)
    LambdaInference.java:16: error: cannot find symbol
        Foo.foo(value -> true).booleanValue(); // Compile error here
                              ^
      symbol:   method booleanValue()
      location: class Object
    1 error
    
    
    

    This is a lot of information, let's break it down.

    LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
        Foo.foo(value -> true).booleanValue(); // Compile error here
           ^
      phase: BASIC
      with actuals: 
      with type-args: no arguments
      candidates:
          #0 applicable method found: foo(Bar)
            (partially instantiated to: (Bar)Object)
      where T is a type-variable:
        T extends Object declared in method foo(Bar)
    
    
    

    phase: method applicability phase
    actuals: the actual arguments passed in
    type-args: explicit type arguments
    candidates: potentially applicable methods

    actuals is because our implicitly typed lambda is not pertinent to applicability.

    The compiler resolves your invocation of foo to the only method named foo in Foo. It has been partially instantiated to Foo. foo (since there were no actuals or type-args), but that can change at the deferred-inference stage.

    LambdaInference.java:16: Note: Deferred instantiation of method foo(Bar)
        Foo.foo(value -> true).booleanValue(); // Compile error here
               ^
      instantiated signature: (Bar)Object
      target-type: 
      where T is a type-variable:
        T extends Object declared in method foo(Bar)
    
    
    

    instantiated signature: the fully instantiated signature of foo. It is the result of this step (at this point no more type inference will be made on the signature of foo).
    target-type: the context the call is being made in. If the method invocation is a part of an assignment, it will be the left hand side. If the method invocation is itself part of a method invocation, it will be the parameter type.

    Since your method invocation is dangling, there is no target-type. Since there is no target-type, no more inference can be done on foo and T is inferred to be Object.


    Analysis

    The compiler does not use implicitly typed lambdas during inference. To a certain extent, this makes sense. In general, given param -> BODY, you will not be able to compile BODY until you have a type for param. If you did try to infer the type for param from BODY, it might lead to a chicken-and-egg type problem. It's possible that some improvements will be made on this in future releases of Java.


    Solutions

    Foo. foo(value -> true)

    This solution provides an explicit type argument to foo (note the with type-args section below). This changes the partial instantiation of the method signature to (Bar)Boolean, which is what you want.

    LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
        Foo. foo(value -> true).booleanValue(); // Compile error here
           ^
      phase: BASIC
      with actuals: 
      with type-args: Boolean
      candidates:
          #0 applicable method found: foo(Bar)
            (partially instantiated to: (Bar)Boolean)
      where T is a type-variable:
        T extends Object declared in method foo(Bar)
    LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
        Foo. foo(value -> true).booleanValue(); // Compile error here
                                        ^
      phase: BASIC
      with actuals: no arguments
      with type-args: no arguments
      candidates:
          #0 applicable method found: booleanValue()
    

    Foo.foo((Value value) -> true)

    This solution explicitly types your lambda, which allows it to be pertinent to applicability (note with actuals below). This changes the partial instantiation of the method signature to (Bar)Boolean, which is what you want.

    LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
        Foo.foo((Value value) -> true).booleanValue(); // Compile error here
           ^
      phase: BASIC
      with actuals: Bar
      with type-args: no arguments
      candidates:
          #0 applicable method found: foo(Bar)
            (partially instantiated to: (Bar)Boolean)
      where T is a type-variable:
        T extends Object declared in method foo(Bar)
    LambdaInference.java:16: Note: Deferred instantiation of method foo(Bar)
        Foo.foo((Value value) -> true).booleanValue(); // Compile error here
               ^
      instantiated signature: (Bar)Boolean
      target-type: 
      where T is a type-variable:
        T extends Object declared in method foo(Bar)
    LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
        Foo.foo((Value value) -> true).booleanValue(); // Compile error here
                                               ^
      phase: BASIC
      with actuals: no arguments
      with type-args: no arguments
      candidates:
          #0 applicable method found: booleanValue()
    

    Foo.foo((Bar) value -> true)

    Same as above, but with a slightly different flavor.

    LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
        Foo.foo((Bar) value -> true).booleanValue(); // Compile error here
           ^
      phase: BASIC
      with actuals: Bar
      with type-args: no arguments
      candidates:
          #0 applicable method found: foo(Bar)
            (partially instantiated to: (Bar)Boolean)
      where T is a type-variable:
        T extends Object declared in method foo(Bar)
    LambdaInference.java:16: Note: Deferred instantiation of method foo(Bar)
        Foo.foo((Bar) value -> true).booleanValue(); // Compile error here
               ^
      instantiated signature: (Bar)Boolean
      target-type: 
      where T is a type-variable:
        T extends Object declared in method foo(Bar)
    LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
        Foo.foo((Bar) value -> true).booleanValue(); // Compile error here
                                             ^
      phase: BASIC
      with actuals: no arguments
      with type-args: no arguments
      candidates:
          #0 applicable method found: booleanValue()
    

    Boolean b = Foo.foo(value -> true)

    This solution provides an explicit target for your method call (see target-type below). This allows the deferred-instantiation to infer that the type parameter should be Boolean instead of Object (see instantiated signature below).

    LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
        Boolean b = Foo.foo(value -> true);
                       ^
      phase: BASIC
      with actuals: 
      with type-args: no arguments
      candidates:
          #0 applicable method found: foo(Bar)
            (partially instantiated to: (Bar)Object)
      where T is a type-variable:
        T extends Object declared in method foo(Bar)
    LambdaInference.java:16: Note: Deferred instantiation of method foo(Bar)
        Boolean b = Foo.foo(value -> true);
                           ^
      instantiated signature: (Bar)Boolean
      target-type: Boolean
      where T is a type-variable:
        T extends Object declared in method foo(Bar)
    
    
    

    Disclaimer

    This is the behavior that's occurring. I don't know if this is what is specified in the JLS. I could dig around and see if I could find the exact section that specifies this behavior, but type inference notation gives me a headache.

    This also doesn't fully explain why changing Bar to use a raw Value would fix this issue:

    LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
        Foo.foo(value -> true).booleanValue();
           ^
      phase: BASIC
      with actuals: 
      with type-args: no arguments
      candidates:
          #0 applicable method found: foo(Bar)
            (partially instantiated to: (Bar)Object)
      where T is a type-variable:
        T extends Object declared in method foo(Bar)
    LambdaInference.java:16: Note: Deferred instantiation of method foo(Bar)
        Foo.foo(value -> true).booleanValue();
               ^
      instantiated signature: (Bar)Boolean
      target-type: 
      where T is a type-variable:
        T extends Object declared in method foo(Bar)
    LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
        Foo.foo(value -> true).booleanValue();
                              ^
      phase: BASIC
      with actuals: no arguments
      with type-args: no arguments
      candidates:
          #0 applicable method found: booleanValue()
    
    
    

    For some reason, changing it to use a raw Value allows the deferred instantiation to infer that T is Boolean. If I had to speculate, I would guess that when the compiler tries to fit the lambda to the Bar, it can infer that T is Boolean by looking at the body of the lambda. This implies that my earlier analysis is incorrect. The compiler can perform type inference on the body of a lambda, but only on type variables that only appear in the return type.

    提交回复
    热议问题