在使用方法引用时,理解与类型推断相关的错误消息



我想从字符串中创建一个非字母字符列表,所以我写了:

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

然而,这会抛出以下错误消息:

不存在类型变量的实例,因此Character符合整数推理变量T具有不兼容的边界:相等约束:整数下限:字符

我没有完全理解错误消息,但我认为这是由Character#isAlphabeticint codePoint作为参数而不是字符引起的,因为将Character::isAlphabetic替换为Character::isUpperCase(例如)可以很好地接收char

现在,如果我写:

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

它编译得很好,我甚至没有那么惊讶/困惑。但是,如果我写

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

它的编译也很好,这让我很困惑,因为Character::isAlphabetic基本上不等于c -> Character.isAlphabetic(c)吗?嗯,显然不是在所有情况下(因为AFAIK,它在大多数情况下)

所以我的两个问题是:

  1. 这个错误消息到底在说什么?我确实在一定程度上理解它,但肯定不是完全理解
  2. 为什么第一个版本不起作用,而第三个版本起作用

Character::isAlphabeticc -> Character.isAlphabetic(c)之间的区别在于,由于Character.isAlphabetic(int)不是重载方法,前者是精确的方法引用,而后者是隐式类型的lambda表达式。

我们可以证明,不精确的方法引用被接受的方式与隐式类型的lambda表达式相同:

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");
}
}

编译器接受了这一点。

然而,这并不意味着这种行为是正确的。精确的方法参考可能有助于过载选择,而不精确的方法则没有,如§15.12.2所规定。:

某些包含隐式类型lambda表达式(§15.27.1)或不精确方法引用(§15.13.1)的参数表达式被适用性测试忽略,因为在选择调用的目标类型之前无法确定其含义。

相比之下,当涉及到15.13.2。方法引用的类型,在提到的精确方法引用和不精确方法引用之间没有区别。只有目标类型决定了方法引用的实际类型(假设目标类型是一个函数接口,并且方法引用是一致的)。

因此,以下工作没有问题:

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);
}
}

当然,这不是最初的程序逻辑

这里,Character::isAlphabetic仍然是一个精确的方法参考,但它与目标类型Predicate<Character>一致,因此它的工作原理与没有什么不同

Predicate<Character> p = Character::isAlphabetic;

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

将泛型方法插入到方法调用的嵌套中并不会阻止类型推理的正常工作。正如在回答类似的脆弱类型推理问题时所讨论的那样,我们可以插入一个对结果类型没有贡献的通用方法,而不会出现问题:

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);
}
}

甚至通过插入方法来"修复"原始代码的问题

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);
}
}

重要的是,Predicate<Character>Predicate<Integer>之间没有子类型关系,因此dummy方法无法在它们之间进行转换。它只是返回与编译器为其参数推断的类型完全相同的类型。

我认为编译器错误是一个错误,但正如我在另一个答案中所说,在我看来,即使规范支持这种行为,它也应该得到纠正。


顺便说一句,对于这个特定的例子,我会使用

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

无论如何,通过这种方式,您不是将int值装箱到Character对象,只是为了在谓词中再次将它们取消装箱到int,而是仅在通过筛选器后装箱值。

使用OCP Java 8:一书中的示例

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

作者说明:第1行引用的列表可以是List或List,或者列表第2行没有编译,因为我们可能有一个List和Exception对象不适合在其中。

您给出的相同示例:

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

所以我们有一个:Predicate<? super Character>,使用方法引用Predicate.not(Character::isAlphabetic)将从Character转换为int,下一个int将自动装箱为Integer,所以我们将有一个Integer实例,但我们的下界不能接受Integer对象,它可以接受Character实例(它允许将超级引用作为Integer、Object,但不允许超级实例)。您在这里试图将一个实例从超类型传递到下界泛型,这会混淆编译器。

使用以下代码:c -> Character.isAlphabetic(c),您声明接受Character(Character实例和Character引用)并将其传递给方法。

Java将执行安全的基元到基元转换。

这个

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

为这些自动操作提供2机会:

  • 一个用于lambda参数的过滤器Object,以及
  • 一个用于lambda参数到isAlphabetic()参数

允许编译。

第一个操作是取消对Characterchar的装箱,使lambda参数成为char。第二个操作(安全地)将char强制转换为int

最新更新