I am trying to use method references with generic classes and have noticed that I can make it work with the equivalent lambda expression, but not with method references. I'm wondering if someone can point out why or tell me what I'm doing wrong...
What I was trying to accomplish when I ran into this issue was to be able to have different builders for different things. The builder receives raw data and a schema containing method references and instructions on when those methods should be called.
I created sandbox code to identify the problem I was having:
public class Builder<T extends List> {
public void doA(T list) {
System.out.println("doA");
}
}
public class CarBuilder extends Builder<ArrayList> {
public void doB(ArrayList list) {
System.out.println("doB");
}
}
If I create the schemas like this:
public class Schema {
BiConsumer<? extends Builder, ? extends List> methodReference;
public static Schema builderSchema = new Schema(Builder::doA);
public static Schema carBuilderSchema = new Schema(CarBuilder::doB);
private Schema(BiConsumer<? extends Builder, ? extends List> methodReference) {
this.methodReference = methodReference;
}
}
It does not compile:
Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project Sandbox: Compilation failure
com/sandbox/Schema.java:[11,56] incompatible types: invalid method reference
method doB in class com.sandbox.CarBuilder cannot be applied to given types
required: java.util.ArrayList
found: com.sandbox.Builder,java.util.List
reason: actual and formal argument lists differ in length
If I change it to use lambda expressions instead, it compiles:
public class Schema {
BiConsumer<? extends Builder, ? extends List> methodReference;
public static Schema builderSchema = new Schema((Builder builder, List list) -> builder.doA(list));
public static Schema carBuilderSchema = new Schema((CarBuilder builder, ArrayList list) -> builder.doB(list));
private Schema(BiConsumer<? extends Builder, ? extends List> methodReference) {
this.methodReference = methodReference;
}
}
From what I've read, I thought this was exactly the same as above!?
Method references and explicitly-typed lambda expressions are treated differently.
Let's simplify the example to just:
public class JavaClass {
public static void main(String[] args) {
Consumer<? /* extends Object*/> a = JavaClass::f; // error
a = (String s) -> JavaClass.f(s); // works
Consumer<String> b = JavaClass::f;
a = b; // also works
}
static void f(String s) {}
}
This code has the same issues as your code with CarBuilder
and ArrayList
.
To determine whether a method reference is compatible with a functional interface type, we first need to figure out the function type to check against. For a method reference, this function type is derived from the target type Consumer<?>
. This turns out to be a function returning void
, and taking an Object
as parameter. See Function Types for details.
Then, in a similar way as overload resolution, we need to resolve the method reference - find a suitable method with the name f
that is compatible with the function type. Here, the method reference is treated as if it were a method call with an argument of type Object
, i.e.
void temp(Object o) {
// if overload resolution successfully finds an overload for this call,
// that overload is what the method reference is referencing
JavaClass.f(o);
}
Of course, JavaClass.f
takes a String
, so this does not work, and that's the reason for the error.
Explicitly-typed lambda expressions are handled differently. For a functional interface type with wildcards like Consumer<?>
here, Functional Interface Parameterization Inference is used to find the function type to check against. This basically takes into account the parameter types you explicitly stated in the lambda expression. This time, we find the function type to be a function taking a String
and returning void
. The lambda expression is trivially compatible with that.
If the lambda expression had been implicitly-typed, (s) -> JavaClass.f(s)
, it would have been treated in a similar way to a method reference.
So to use a method reference here, you would need a cast:
Consumer<?> a = (Consumer<String>)JavaClass::f;
new Schema((BiConsumer<CarBuilder, ArrayList>)CarBuilder::doB);
See also Type of a Lambda Expression and Type of a Method Reference.
That said, using a ? extends
bound on a Consumer
doesn't make much sense - the consumer wouldn't be able to consume anything except null
, without unchecked casts. Remember producers extends
, consumers super
.
You should probably stop using raw types too.