Making sense of error message related to type inference when using a method reference

Viewed 241

I wanted to create a list of non-alphabetic characters from a string, so I wrote:

str.chars()
        .mapToObj(c -> (char) c)
        .filter(Predicate.not(Character::isAlphabetic))
        .toList();

However this throws the following error message:

no instance(s) of type variable(s) exist so that Character conforms to Integer inference variable T has incompatible bounds: equality constraints: Integer lower bounds: Character

I didn't fully understand the error message, but I figured that it is caused by Character#isAlphabetic taking in int codePoint as a parameter instead of a char because replacing Character::isAlphabetic with Character::isUpperCase (for example) which takes in a char works fine.

Now, if I write:

str.chars()
        .mapToObj(c -> (char) c)
        .filter(c -> !Character.isAlphabetic(c))
        .toList();

it compiles just fine, and I'm not even that surprised/confused. However, if I write

str.chars()
        .mapToObj(c -> (char) c)
        .filter(Predicate.not(c -> Character.isAlphabetic(c)))
        .toList();

it also compiles just fine, which definitely confuses me because isn't Character::isAlphabetic basically equivalent to c -> Character.isAlphabetic(c)? Well, apparently it isn't in all cases (because AFAIK it is in most)

So my 2 questions are:

  1. What exactly is this error message saying? I do understand it to an extent but definitely not completely
  2. Why does the first version not work but the third does?
3 Answers

The difference between Character::isAlphabetic and c -> Character.isAlphabetic(c) is that since Character.isAlphabetic(int) is not an overloaded method, the former is an exact method reference whereas the latter is an implicitly typed lambda expression.

We can show that an inexact method reference is accepted the same way as an implicitly typed lambda expression:

class SO71643702 {
    public static void main(String[] args) {
        String str = "123abc456def";
        List<Character> l = str.chars()
            .mapToObj(c -> (char) c)
            .filter(Predicate.not(SO71643702::isAlphabetic))
            .toList();
        System.out.println(l);
    }

    public static boolean isAlphabetic(int codePoint) {
        return Character.isAlphabetic(codePoint);
    }

    public static boolean isAlphabetic(Thread t) {
      throw new AssertionError("compiler should never choose this method");
    }
}

This is accepted by the compiler.

However, this doesn’t imply that this behavior is correct. Exact method references may help in overload selection where inexact do not, as specified by §15.12.2.:

Certain argument expressions that contain implicitly typed lambda expressions (§15.27.1) or inexact method references (§15.13.1) are ignored by the applicability tests, because their meaning cannot be determined until the invocation's target type is selected.

In contrast, when it comes to the 15.13.2. Type of a Method Reference, there is no difference between exact and inexact method references mentioned. Only the target type determines the actual type of the method reference (assuming that the target type is a functional interface and the method reference is congruent).

Consequently, the following works without problems:

class SO71643702 {
    public static void main(String[] args) {
        String str = "123abc456def";
        List<Character> l = str.chars()
            .mapToObj(c -> (char) c)
            .filter(Character::isAlphabetic)
            .toList();
        System.out.println(l);
    }
}

Of course, that’s not the original program logic

Here, Character::isAlphabetic still is an exact method reference, but it’s congruent with the target type Predicate<Character>, so it works not different to

Predicate<Character> p = Character::isAlphabetic;

or

Predicate<Character> p = (Character c) -> Character.isAlphabetic(c);

It’s not as if the insertion of a generic method into nesting of method invocations will stop the type inference from working in general. As discussed in this answer to a similar fragile type inference issue, we can insert a generic method not contributing to the resulting type without problems:

class SO71643702 {
    static <X> X dummy(X x) { return x; }

    public static void main(String[] args) {
        String str = "123abc456def";
        List<Character> l = str.chars()
            .mapToObj(c -> (char) c)
            .filter(dummy(Character::isAlphabetic))
            .toList();
        System.out.println(l);
    }
}

and even “fix” the problem of the original code by inserting the method

class SO71643702 {
    static <X> X dummy(X x) { return x; }

    public static void main(String[] args) {
        String str = "123abc456def";
        List<Character> l = str.chars()
            .mapToObj(c -> (char) c)
            .filter(Predicate.not(dummy(Character::isAlphabetic)))
            .toList();
        System.out.println(l);
    }
}

It’s important that there is no subtype relationship between Predicate<Character> and Predicate<Integer>, so the dummy method can not translate between them. It’s just returning exactly the same type as the compiler inferred for its argument.

I consider the compiler error a bug, but as I said at the other answer, even if the specification backs up this behavior, it should get corrected, in my opinion.


As a side note, for this specific example, I’d use

var l = str.chars()
    .filter(c -> !Character.isAlphabetic(c))
    .mapToObj(c -> (char)c)
    .toList();

anyway, as this way, you’re not boxing int values to Character objects, just to unbox them to int again in the predicate, but rather, only box values after passing the filter.

Using an example from the book OCP Java 8 :

1: List<? super IOException> exceptions = new ArrayList<Exception>();
2: exceptions.add(new Exception()); // DOES NOT COMPILE

Explanation of writers : Line 1 reference a List that could be List or List or List. Line 2 does not compile because we could have a List and an Exception object wouldn’t fit in there.

The same example given by you :

"hello".chars()
        .mapToObj(c -> (char) c) // generate a  Stream<Character>
        .filter(Predicate.not(Character::isAlphabetic))
 // filter signature :  Stream<T> filter(Predicate<? super T> predicate);

So we have a : Predicate<? super Character> , using the method reference Predicate.not(Character::isAlphabetic) will do the conversion from Character to int and next int will be autoboxed to Integer , so we will have an Integer instance, but our lower bound cannot accept an Integer Objects , it can accept a Character instances (it allow super references as Integer ,Object but not a Super Instances ). You are trying here to passe an instance from a super type to a lower bound generics and this will confuse the compiler.

Using this code : c -> Character.isAlphabetic(c) , you are declaring that you accept the Character (Character instance and Character reference) and passing it to the method.

Java will either autobox/autounbox or perform a safe primitive-to-primitive cast.

This

filter(Predicate.not(c -> Character.isAlphabetic(c)

offers 2 opportunities for these automatic operations:

  • one for the filter Object to the lambda parameter, and
  • one for the lambda parameter to the isAlphabetic() parameter

which allows this to compile.

The first operation is to unbox Character to char to make the lambda parameter a char. The second operation (safely) casts char to int.

Related