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

Sui*_*uic 7 java java-stream

我想从字符串创建非字母字符列表,所以我写道:

str.chars()
        .mapToObj(c -> (char) c)
        .filter(Predicate.not(Character::isAlphabetic))
        .toList();
Run Code Online (Sandbox Code Playgroud)

但是,这会引发以下错误消息:

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

我没有完全理解错误消息,但我认为这是由于作为参数而不是 charCharacter#isAlphabetic进行替换而导致的,因为替换为(例如),它可以正常工作。int codePointCharacter::isAlphabeticCharacter::isUpperCasechar

现在,如果我写:

str.chars()
        .mapToObj(c -> (char) c)
        .filter(c -> !Character.isAlphabetic(c))
        .toList();
Run Code Online (Sandbox Code Playgroud)

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

str.chars()
        .mapToObj(c -> (char) c)
        .filter(Predicate.not(c -> Character.isAlphabetic(c)))
        .toList();
Run Code Online (Sandbox Code Playgroud)

它也编译得很好,这肯定让我感到困惑,因为它Character::isAlphabetic基本上不等于c -> Character.isAlphabetic(c)?好吧,显然并非在所有情况下(因为据我所知,在大多数情况下)

所以我的两个问题是:

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

Hol*_*ger 5

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

\n

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

\n
class SO71643702 {\n    public static void main(String[] args) {\n        String str = "123abc456def";\n        List<Character> l = str.chars()\n            .mapToObj(c -> (char) c)\n            .filter(Predicate.not(SO71643702::isAlphabetic))\n            .toList();\n        System.out.println(l);\n    }\n\n    public static boolean isAlphabetic(int codePoint) {\n        return Character.isAlphabetic(codePoint);\n    }\n\n    public static boolean isAlphabetic(Thread t) {\n      throw new AssertionError("compiler should never choose this method");\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这是编译器接受的。

\n

但是,这并不意味着此行为是正确的。精确方法引用可能有助于重载选择,而不精确方法引用则不然,如\xc2\xa715.12.2 所指定。

\n
\n

适用性测试将忽略包含隐式类型 lambda 表达式 ( \xc2\xa715.27.1 ) 或不精确方法引用 ( \xc2\xa715.13.1 ) 的某些参数表达式,因为只有调用的目标类型才能确定它们的含义被选中。

\n
\n

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

\n

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

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

当然,那个\xe2\x80\x99并不是原来的程序逻辑

\n

这里,Character::isAlphabetic仍然是一个精确的方法引用,但它\xe2\x80\x99s与目标类型一致Predicate<Character>,所以它的工作原理与

\n
Predicate<Character> p = Character::isAlphabetic;\n
Run Code Online (Sandbox Code Playgroud)\n

或者

\n
Predicate<Character> p = (Character c) -> Character.isAlphabetic(c);\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x80\x99 似乎将泛型方法插入方法调用的嵌套中不会阻止类型推断的正常工作。正如在类似的脆弱类型推断问题的答案中所讨论的,我们可以插入一个对结果类型没有贡献的泛型方法,而不会出现问题:

\n
class SO71643702 {\n    static <X> X dummy(X x) { return x; }\n\n    public static void main(String[] args) {\n        String str = "123abc456def";\n        List<Character> l = str.chars()\n            .mapToObj(c -> (char) c)\n            .filter(dummy(Character::isAlphabetic))\n            .toList();\n        System.out.println(l);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

甚至 \xe2\x80\x9cfix\xe2\x80\x9d 通过插入方法解决原始代码的问题

\n
class SO71643702 {\n    static <X> X dummy(X x) { return x; }\n\n    public static void main(String[] args) {\n        String str = "123abc456def";\n        List<Character> l = str.chars()\n            .mapToObj(c -> (char) c)\n            .filter(Predicate.not(dummy(Character::isAlphabetic)))\n            .toList();\n        System.out.println(l);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

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

\n
\n

作为旁注,对于这个特定示例,I\xe2\x80\x99d 使用

\n
var l = str.chars()\n    .filter(c -> !Character.isAlphabetic(c))\n    .mapToObj(c -> (char)c)\n    .toList();\n
Run Code Online (Sandbox Code Playgroud)\n

无论如何,这样,您\xe2\x80\x99 就不会将int值装箱到Character对象,只是为了在谓词中再次将它们拆箱int,而是仅在通过过滤器后将值装箱。

\n