4

I'm currently using a library method that takes a functional interface with a generic wildcard type as a method parameter (specifically, RecursiveComparisonAssert.withEqualsForFields​(BiPredicate<?,​?> equals, String... fieldLocations) in the AssertJ library). I discovered that when I pass a lambda method argument that uses any type other than Object for the wildcard type parameter, I get a compilation error. For instance, if the method is sameInstant(Instant i1, Instant i2), I get a compiler error when I call withEqualsForFields(this::sameInstant, "someField").

Simplified Example

As a more simple example of this phenomenon not requiring the use of any particular library to reproduce, take the following scenario using a Predicate<?> method parameter:

public class WildcardLambda {
    public static void main(String[] args) {
        wildcardPredicateInput(WildcardLambda::objectPredicate);
        wildcardPredicateInput(WildcardLambda::stringPredicate); // Fails
        wildcardPredicateInput((Predicate<String>) WildcardLambda::stringPredicate);
        wildcardPredicateInput(input -> stringPredicate(input));

        genericPredicateInput(WildcardLambda::objectPredicate);
        genericPredicateInput(WildcardLambda::stringPredicate);
    }

    private static void wildcardPredicateInput(Predicate<?> predicate) {}
    private static <T> void genericPredicateInput(Predicate<T> predicate) {}

    private static boolean objectPredicate(Object input) { return true; }
    private static boolean stringPredicate(String input) { return true; }
}

Attempting to pass a lambda that would be a Predicate<String> to a method that accepts a Predicate<?> results in a compilation error:

$ javac WildcardLambda.java -Xdiags:verbose
WildcardLambda.java:6: error: method wildcardPredicateInput in class WildcardLambda cannot be applied to given types;
        wildcardPredicateInput(WildcardLambda::stringPredicate); // Fails
        ^
  required: Predicate<?>
  found:    WildcardLa[...]icate
  reason: argument mismatch; invalid method reference
      method stringPredicate in class WildcardLambda cannot be applied to given types
        required: String
        found:    Object
        reason: argument mismatch; Object cannot be converted to String
1 error

However, passing a lambda that is a Predicate<Object> or explicitly casting the lambda to Predicate<String> succeed. Additionally, this works if it is passed to a generic method that expects a Predicate<T>.

Why does this lambda usage result in a compilation error? Is there something I'm overlooking in the JLS that indicates that this should fail to compile?

M. Justin
  • 14,487
  • 7
  • 91
  • 130
  • Something I don't understand: shouldn't `BiPredicate` take two arguments? I only see you supplying one in `stringPredicate`. I don't know if this directly affects the error however. – markspace Jun 30 '21 at 19:58
  • @markspace Sorry for the confusion: I started out explaining where I ran into the issue in the wild (which used `BiPredicate`), then followed up with a simplified version of the same issue (which used `Predicate`). I'll update the text to make that clearer. – M. Justin Jun 30 '21 at 20:00
  • I'm going to guess that it's just the way generics work, `?` is usually only substitutable for `Object`. Possibly this looks like a design error in the library, they should have used ` assert( BiPredicate super X, ? super X>` or something like that. However I should let an actual expert chime in with a definitive answer. – markspace Jun 30 '21 at 20:10

1 Answers1

4

This is a known type inference problem. As described in JLS 18.5.3:

In order to determine the function type of a wildcard-parameterized functional interface, we have to "instantiate" the wildcard type arguments with specific types. The "default" approach is to simply replace the wildcards with their bounds, as described in §9.8, but this produces spurious errors in cases where a lambda expression has explicit parameter types that do not correspond to the wildcard bounds.

Here the wildcard type argument gets "instantiated" to its bound Object, and because Predicate<String> is not a subtype of Predicate<Object>, the method is considered not applicable.

Providing the type hint, via a cast (as in the post) or a local variable (as below), solves the problem.

Predicate<String> myPredicate = WildcardLambda::stringPredicate;
wildcardPredicateInput(myPredicate);
SDJ
  • 4,083
  • 1
  • 17
  • 35