javagenericslambdamethod-reference

Java error when passing in generic method parameter


public static class MyClass {
    public String methodS(UnaryOperator<String> fn) {
        return fn.apply("");
    }
    public Integer methodI(UnaryOperator<Integer> fn) {
        return fn.apply(0);
    }
    public <T> T identity(T t) {
        return t;
    }
    public <T> void test(UnaryOperator<T> fn) {
        String s;
        s = methodS(UnaryOperator.identity()); // OK
        s = methodS(this::identity);           // OK
        s = methodS(String::trim);             // OK
        s = methodS(fn::apply);                // Fail
        Integer i;
        i = methodI(UnaryOperator.identity()); // OK
        i = methodI(this::identity);           // OK
        i = methodI(Math::abs);                // OK
        i = methodI(fn::apply);                // Fail
    }
    public void goal() {
        test(o -> {
            doSomething(o);
            return o;
        });
    }
}

I wonder why passing class method references OK but passing a method parameter not in the above example? How do you fix it?

The goal is to be able to call like in the goal() method, where a generic lambda can be written to take on the common base class object (in this case Object).


Edited: some misunderstood the question, probably because of Object as the base class. Here might be a better example with some custom common base class:

public static class MyClass {
    public static class ExtS extends MyClass {};
    public static class ExtI extends MyClass {};
    public void common() {}
    public ExtS methodS(UnaryOperator<ExtS> fn) {
        return fn.apply(new ExtS());
    }
    public ExtI methodI(UnaryOperator<ExtI> fn) {
        return fn.apply(new ExtI());
    }
    public <X extends MyClass> X identity(X x) {
        x.common(); // can call MyClass methods
        return x;
    }
    public <Y extends MyClass> void test(UnaryOperator<Y> fn) {
        ExtS s;
        s = methodS(UnaryOperator.identity());    // OK
        s = methodS(this::identity);              // OK
        s = methodS(t -> (ExtS) fn.apply((Y) t)); // OK
        s = methodS(fn::apply);                   // fail
        ExtI i;
        i = methodI(UnaryOperator.identity());    // OK
        i = methodI(this::identity);              // OK
        i = methodI(t -> (ExtI) fn.apply((Y) t)); // OK
        i = methodI(fn::apply);                   // fail
    }
    public void goal() {
        test(y -> {
            y.common(); // can call MyClass methods
            return y;
        });
        test(this::identity); // doing the same as above
    }
}

Solution

  • You should step back and try doing actual assignments. When you try to do methodI all you are doing is assigning the argument to UnaryOperator<ExI>.

    static <T> UnaryOperator<T> identity()
    

    That means the T is assigned when you call identity.

    methodI(UnaryOperator::identity);
    

    This is good, that is the point of the identity, you call it with an argument and it will take that generic. When you call methodI it assigns the generic of UnaryOperator.identity as ExtI.

    methodI(this::identity);
    

    Same as the previous example, it takes the type when you call identity so the type is not defined until you assign it in the argument of methodI.

    methodI(fn::apply);
    

    This is not good because you have already assigned the type of fn. It is Y all you know about Y is that it is a MyClass and it's parents. That means methodI is trying to assign. UnaryOperatory<ExtI> fn to UnaryOperator<? extends MyClass>. They're not the same thing. But they could be. Which is why

    methodI(t -> (ExtI) fn.apply((Y) t));
    

    works. Your constraints don't say what you're doing is wrong, they just don't guarantee correctness. If you use explicit casts you can "make it work".

    Consider this simple class.

    class Example<T>{
    
        T apply( T t ){
            return t;
        }
        <U> U identity( U u){
            return u;
        }
     }
    
    Example<String> ex = new Example();
    ex.apply("one"); 
    

    That works because apply works on String.

    ex.apply(2); 
    

    Broken because ex is parameterized by a String.

    ex.identity(2); 
    

    Works because identity takes the parameter type when it is called.