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.