I am struggling to understand how variance works in Java.
In the following example, I define a function test which takes a Consumer. The function is defined without contravariance, so I would expect that Consumer<Object> is not a subtype of Consumer<Pair<Animal, Animal>>. Yet, the code compiles, and test accepts the lambda Variance:::superAction.
What am I missing?
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import java.util.function.Consumer;
public class Variance {
public static void main(String[] args) {
test(Variance::exactMatchAction);
test(Variance::superAction);
}
private static void exactMatchAction(Pair<Animal, Animal> pair) {
System.out.println(pair.getLeft().getClass().getName());
}
private static void superAction(Object obj) {
System.out.println(obj.getClass().getName());
}
private static void test(Consumer<Pair<Animal, Animal>> action) {
action.accept(ImmutablePair.of(new Animal(), new Animal()));
action.accept(ImmutablePair.of(new Dog(), new Dog()));
}
static class Animal { }
static class Dog extends Animal { }
}
Edit: Per @Thielo's comment, the reference superAction is desugared to a Consumer<Pair<Animal, Animal>> NOT a Consumer<Object>.
The correct type to give the test method is something like:
void test(Consumer<? super Pair<? extends Animal, ? extends Animal>>)
This type will allow us to pass a Consumer<Object> to test, and also allow us to call the consumer with arguments like Pair<Dog, Dog> instead of just Pair<Animal, Animal>.
As a follow-up question, with this updated type for test, it will not accept a method reference like void exactMatchAction<Pair<Animal, Animal>> anymore, only void exactMatchAction<Pair<? extends Animal, ? extends Animal>>. Why is this?
Method reference expressions (such as your Variance::superAction) are poly expressions (JLS8, 15.13). The type of a poly expression may be influenced by the expression's target type (JLS8, 15.3), which is the type expected in that context (JLS8, 5), i.e., Consumer<Pair<Animal, Animal>>, in your case.
The details are spelled out in JLS8, 15.13.2. The basic idea is that there is special handling for functional interface types such as Consumer. Specifically, the method type only need be congruent to the function type (which is Pair<Animal, Animal> -> void-- note that Consumer has vanished from the type consideration here), which is satisfied by "identif[ying] a single compile-time declaration corresponding to the reference" (and having void as return type). Here, the notion of "identifying" a declaration goes back to 15.12.2 and basically describes the method overload resolution process. In other words, the language now takes the function parameters expected for Consumer<Pair<Animal, Animal>>.accept() (i.e., Pair<Animal, Animal>) and checks whether the method reference could be called with that (this resolves overloading in case there are multiple static methods with the same name).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With