0
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
    }
}
25
  • 1
    Someone can call test(String::trim). What would methodI(fn::apply); do in that case? The type would be incorrect. You cannot make a similar argument for this::identity. Commented Dec 4, 2023 at 12:47
  • 2
    Your goal is impossible to achieve. There is no syntax to declare that T must be a common supertype of Integer and String. Commented Dec 4, 2023 at 13:37
  • 2
    Why would you expect it to work? fn::apply is a UnaryOperator that takes a Y. That Y is a subclass of MyClass. methodI requires a UnaryOperator that is a specific subclass of MyClass namely ExtI. Commented Dec 4, 2023 at 15:42
  • 2
    Do you notice that there is no “<Y>” in Y apply(Y y)? This method does not declare a type parameter—it uses an existing type parameter (from the UnaryOperator type). This method is not generic. In contrast, <X extends MyClass> X identity(X x) does declare a type parameter; this is a generic method. Fundamentally different. Commented Dec 5, 2023 at 10:30
  • 2
    The type is adaptive and your declaration <Y extends MyClass> void test(UnaryOperator<Y> fn) allows to choose a type argument for Y, however, it’s the caller of test who chooses what Y is. You can’t choose the method signature of fn::apply inside the test method because the caller already did. You still can create arbitrarily typed UnaryOperators inside the method; the type is as generic as the method identityis. The crucial point is who chooses the actual type arguments for a particular generic construct. For the fn parameter, it’s the caller of the method. Commented Dec 5, 2023 at 10:53

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks heaps. Almost there, I think I am hitting something. I can see assigning UnaryOperatory<ExtI> to UnaryOperator<? extends MyClass> surely shouldn't be allowed, and OK that Java interprets it that way for <Y extends MyClass> void test(UnaryOperator<Y> fn). But if we look into UnaryOperator#apply, it is defined as Y apply(Y y), which I can't tell the difference from <X extends MyClass> X identity(X x), why Java does not interpret identity the same?
One is at the class/interface level. Y has already been defined for apply when you created an instance of the interface. X is at the method level and gets assigned when you call identity or assign it as a UnaryOperator with a type which is closer to your example.
OK, that surely is a difference that I never thought of, and I believe this can be the answer to my question. But why a class/interface level generic parameter is interpreted differently from a method level generic parameter?
I updated the answer to include an example. They're different because you're telling them to be different. When you specify the generic at the method, you're saying I want this Type to be decided by the code that is going to use the method. It gives you some freedom.
Here is an example where you have a parameterized type Collection and it has a parameterized method with a different type. stackoverflow.com/questions/12355941/…

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.