0

I’ve been working with Java 8 lambdas for several months and just now discovered a behavior that I don’t remember seeing any mention of in the Java docs or programming websites. It can be seen in the following code:

public class CompilerGeneratedLambdaExample
{
    static <A,R> R callFunction(Function<A,R> function, A arg) {
        return function.apply(arg);
    }

    static int staticFunction_arg_int_return_int(int i) {
        return i;
    }

    public static void main(String [] args) {

        /* (A) This compiles and executes as expected - stack looks like:
         * 
        Thread [main] (Suspended (breakpoint at line 16 in CompilerGeneratedLambdaExample)) 
        CompilerGeneratedLambdaExample.staticFunction_arg_int_return_int(int) line: 16  
        1268650975.apply(Object) line: not available    
        CompilerGeneratedLambdaExample.callFunction(Function<A,R>, A) line: 11  
        CompilerGeneratedLambdaExample.main(String[]) line: 33  
        */

        Object value = callFunction(CompilerGeneratedLambdaExample::staticFunction_arg_int_return_int,new Integer(10)); 
        System.out.println("value type: " + value.getClass().getName());
        System.out.println("value: " + value);

        /* (B) This will not compile- error message is:
        * 
        The method callFunction(Function<A,R>, A) in the type CompilerGeneratedLambdaExample is not applicable for the arguments (IntUnaryOperator, Integer)
        */ 

        IntUnaryOperator specializedFunction = CompilerGeneratedLambdaExample::staticFunction_arg_int_return_int; // OK
        //Object value = callFunction(specializedFunction,new Integer(10)); // produces compiler error quoted above
    }
}

The comments are based on what I saw with a recent version of the eclipse Java compiler at Java 1.8 compliance level. My summary of the situation is:

(A) if you pass a function object of a specialized type as a method reference to a function that expects the generic counterpart of the type, the complier will wrap the object that handles conversions the arguments and return value.

(B) But if you first assign the method reference to a variable of the specialized type and try to pass the variable to the function that expects the generic type, you get a compiler error indicating a type mismatch.

Behavior (A) provides the equivalent of auto-boxing and unboxing for method references of specialized functional types and is extremely useful for the work I am doing. Behavior (B) would also be useful if it worked, but the fact that it doesn’t seems to be in line with the general workings of the Java object type system.

My question is – where are these two behaviors – and particularly behavior (A) which provides the equivalent of autoboxing and unboxing for method references of specializes function types – treated in the Java documentation, and what other useful discussions of them are there on the web?

Ichigo Kurosaki
  • 3,765
  • 8
  • 41
  • 56

2 Answers2

1

The issue is your assignment to IntUnaryOperator, that is not a Function<Integer, Object>. You could do

Object value2 = callFunction(
        CompilerGeneratedLambdaExample::staticFunction_arg_int_return_int, 
        new Integer(10));

or

Function<Integer, Object> specializedFunction = 
        CompilerGeneratedLambdaExample::staticFunction_arg_int_return_int;
Object value2 = callFunction(specializedFunction, new Integer(10));

or using auto-boxing

Function<Integer, Object> specializedFunction = 
        CompilerGeneratedLambdaExample::staticFunction_arg_int_return_int;
Object value2 = callFunction(specializedFunction, 10);

As for documentation see JLS-15.27. Lambda Expressions

Elliott Frisch
  • 198,278
  • 20
  • 158
  • 249
  • Thanks Elliott. My response to your comment was too long to post as a comment so I posted it as an answer - which it is in my view, but I am interested in further insights.and references to Java doc referring specifically to method references. – John Armstrong Aug 20 '19 at 11:30
0

Thanks for the comment Elliott. The reason I assigned the method reference to a variable of type IntUnaryOperator is that it is an exact match to the signature of the method (staticFunction_arg_int_return_int) which is int -> int. I wasn't looking to fix it, just presenting it as a data point about how Java works. But your alternative assignment offers another datapoint, and it's a very valuable one.

If I add this code (C) to main:

    /* (C)  genericForSpecializedFunction - works - stacktrace:
     * 
    Thread [main] (Suspended (breakpoint at line 15 in CompilerGeneratedLambdaExample)) 
    CompilerGeneratedLambdaExample.staticFunction_arg_int_return_int(int) line: 15  
    575593575.apply(Object) line: not available <- lambda   
    CompilerGeneratedLambdaExample.callFunction(Function<A,R>, A) line: 10  
    CompilerGeneratedLambdaExample.main(String[]) line: 67  

    Output:
    genericForSpecializedFunction: testHelp.CompilerGeneratedLambdaExample$$Lambda$2/575593575@14acaea5 <- same lambda
    (C) value type: java.lang.Integer
    (C) value: 10
    */ 

    Function<Integer,Integer> genericForSpecializedFunction = CompilerGeneratedLambdaExample::staticFunction_arg_int_return_int; // OK
    System.out.println("genericForSpecializedFunction: " + genericForSpecializedFunction);
    Object value3 = callFunction(genericForSpecializedFunction,new Integer(10));
    System.out.println("(C) value type: " + value3.getClass().getName());
    System.out.println("(C) value: " + value3)

I see that the static method staticFunction_arg_int_return_int is wrapped in a lambda just as it is when passed directly to callFunction. In effect, the same conversion, if you want to call it that, is happening in both cases.

Further, if I add code (D) similar to(C), only with a static method that directly matches a non-specialized type:

<code>
    static Integer staticFunction_arg_Integer_return_Integer(Integer i) {
        return i;
    }
    ...
        /* (D) with generic methods - stack
         * 
        Thread [main] (Suspended (breakpoint at line 19 in CompilerGeneratedLambdaExample)) 
        CompilerGeneratedLambdaExample.staticFunction_arg_Integer_return_Integer(Integer) line: 19  
        226710952.apply(Object) line: not available  <- lambda
        CompilerGeneratedLambdaExample.callFunction(Function<A,R>, A) line: 10  
        CompilerGeneratedLambdaExample.main(String[]) line: 84

        Output:
        genericFunction: testHelp.CompilerGeneratedLambdaExample$$Lambda$3/226710952@59fa1d9b <- same lambda
        (D) value type: java.lang.Integer
        (D) value: 10
         */

        Function<Integer,Integer> genericFunction = CompilerGeneratedLambdaExample::staticFunction_arg_Integer_return_Integer; // OK
        System.out.println("genericFunction: " + genericFunction);
        Object value4 = callFunction(genericFunction,new Integer(10));
        System.out.println("(D) value type: " + value4.getClass().getName());
        System.out.println("(D) value: " + value4);
</code>

I see the exact same behavior as (C), even through no argument or return value conversions are needed.

And finally, if I add code (E) reversing the scenario by assigning a generic function to a specialized type:

<code>
    static int callIntUnaryOperator(IntUnaryOperator function, int arg) {
        return function.applyAsInt(arg);
    }
...

        /* (E) specialiedForGenericFunction - stack
         * 
        Thread [main] (Suspended (breakpoint at line 24 in CompilerGeneratedLambdaExample)) 
        CompilerGeneratedLambdaExample.staticFunction_arg_Integer_return_Integer(Integer) line: 24  
        684874119.applyAsInt(int) line: not available <- lambda
        CompilerGeneratedLambdaExample.callIntUnaryOperator(IntUnaryOperator, int) line: 15 
        CompilerGeneratedLambdaExample.main(String[]) line: 109 

        output:
        specializedForgenericFunction: testHelp.CompilerGeneratedLambdaExample$$Lambda$4/684874119@4501b7af <- same lambda
         */

        IntUnaryOperator specializedForGenericFunction = CompilerGeneratedLambdaExample::staticFunction_arg_Integer_return_Integer; // OK
        System.out.println("specializedForgenericFunction: " + specializedForGenericFunction);
        int value5 = callIntUnaryOperator(specializedForGenericFunction,10);
        //System.out.println("(E) value type: " + value5.getClass().getName());
        System.out.println("(E) value: " + value5);
</code>

everything works just as in (C) and (D).

My interpretation of all of this is that:

(1) The functional type of a method reference is not intrinsic to the method reference and is not fully determined by the referenced method (plus or minus class instance for non-static methods). The functional type becomes fixed only when the method reference is assigned to a variable or function parameter or return value with a functional type that is compatible with the type of the referenced method (plus or minus instance). The functional type in question is termed the target type, if I understand correctly.

(2) An important case where the target type is compatible with the type of the referenced method (plus or minus instance) without being the same is that where some argument(s) and/or return value differ in type between primitive (unboxed, specialized) and corresponding object (instantiated generic, boxed).

(3) In this context there is no privileging of object (generic) over primitive (specialized) types. Their relationship is symmetrical, so that the underlying method may have, say, a primitive arg and the functional type an object arg or vice versa, and similarly for the return value. This means that boxing and unboxing can go either way.

(4) I originally thought of the lambdas that appear in the stacktraces and printed functional object values in the example code as wrappers around the underlying methods that exist to perform argument and return value type conversions (particularly boxing and unboxing), but I now see them as simply the concrete realizations of the functional types of the method references as determined by the types of the variables, function parameters or return types that they are being assigned to - the target types. These concrete realizations will always be created and will perform conversions (boxing and unboxing) only as an occasional side effect. Practically speaking, method reference are source code entities that are compiled into lambda objects just as source code lambda expressions are.

I will look at the doc section you link to paying particular attention to references to target types and method references.