5

I am having a java compiler error that I don't understand. It seems that Consumer< ? > and Consumer< T > (with T extends Object) are not equivalent in method signature arguments. Please check out the code below:

import java.util.function.Consumer;

public class MyClass {

    public void joker(Consumer<?> fct) {}
    public <T> void generic(Consumer<T> fct) {}
    public void myConsumer(String s) {}

    public void doesNotCompile()
    {
        joker(this::myConsumer); // COMPILE ERROR : (In Eclipse : ) The type MyClass does not define myConsumer(Object) that is applicable here
        generic(this::myConsumer); // Works fine : how come are "T" and "?" not equivalent here ?

        // The following also works fine as usual :
        Consumer<String> cs = this::myConsumer;
        joker(cs);

        joker((String s) -> myConsumer(s));
    }

}

The error is a bit different when I compile the code via command-line rather than in Eclipse:

D:\>javac -Xdiags:verbose MyClass.java
MyClass.java:11: error: method joker in class MyClass cannot be applied to given types;
                joker(this::myConsumer);
                ^
  required: Consumer<?>
  found:    this::myConsumer
  reason: argument mismatch; invalid method reference
      method myConsumer in class MyClass cannot be applied to given types
        required: String
        found:    Object
        reason: argument mismatch; Object cannot be converted to String
1 error

It is slightly clearer but I still don't get the point. Note you get an analog error with java.util.function.Function but not with java.util.function.Supplier. I therefore believe the error occurs for any functional interface taking in a parameter.

Is this a compiler bug or am I missing something here? In case of the latter (most likely), can someone tell the reason of such a behavior design?

I feel that there is something going wrong with type inference. I also barely know that JVM handles lambdas and method reference differently (AFAIK via MethodHandler). But to be honest, I am just confused.

Help? ¯\ (ツ)

Romadelf
  • 123
  • 4
  • `this::myConsumer` is being considered as a `Consumer`. `Consumer> consumer = this::myConsumer;` won't work – Thiyagu Jun 19 '20 at 07:03

2 Answers2

1

TL;DR; The compiler effectively forbids to pass any value other than null into a Consumer<?>. And thus the usage of Consumer<?> is literally none.

Try this in your joker method:

fct.accept(new Object());

You'll see that it won't compile, because the compiler can't ensure that the passed Consumer really can accept Objects. But using null works, as it is the only valid value for all Objects:

fct.accept(null);

You can overcome this limitation by using unchecked casts, but they are really discouraged, as you will like run into a ClassCastException.

Keep in mind that Consumer<Object> is not equal to Consumer<?>. The prior allows any Object to be passed into it, whereas the latter doesn't accept anything! (except null of course)

Your other example which works "fine":

joker((String s) -> myConsumer(s));

But under the hood you're also making use of unchecked casts. You're implicitly casting s from Object to String. It is almost equivalent to writing:

joker(s -> myConsumer((String) s));

Or even this (Because s is only known to be an Object):

joker((Object s) -> myConsumer((String) s));

To your other question why T and ? are not the same. T is a generic type and the compiler tries to narrow it down to the most specific type. In the case of generic(this::myConsumer), T will be String, if the compiler can't infer the type, it will simply use Object so it is similar to Consumer<Object>, but not Consumer<?>

All in all, using Consumer<?> will likely never have a real use case, because without any nasty hacks you can only pass null into it. And what would be the use of that?

Lino
  • 19,604
  • 6
  • 47
  • 65
1

joker(this::myConsumer); uses the method reference this::myConsumer in a context that forces the target type to be Consumer<?>, where the compiler is forced to use Object for ?. Given that, there would need to be a signature of myConsumer that takes Object, and there isn't any in MyClass.

how come are "T" and "?" not equivalent here ?

That is not the problem. See the following code, which compiles:

Consumer<String> jokerConsumer = this::myConsumer;
joker(jokerConsumer);

The only difference is that the method reference (with its target type) has a compatible method in MyClass.

Besides, if you add public void myConsumer(Object s) {} to your class, the original joker(this::myConsumer); compiles just fine.

The generic method works simply because T is inferred to be the same as the type of the parameter to myConsumer, which puts an end to the problem.

So that shows two ways of solving the current problem. In short, this is a question of the method reference's target type (in the context of the method invocation) being compatible with the signature of the referenced method.

ernest_k
  • 44,416
  • 5
  • 53
  • 99