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

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

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

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

然而,这会引发以下错误消息:

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

我没有完全理解错误消息,但我认为这是由于 Character#isAlphabeticint codePoint 作为参数而不是 char 作为参数引起的,因为将 Character::isAlphabetic 替换为Character::isUpperCase(例如)接受 char 工作正常。

现在,如果我写:

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

它编译得很好,我什至没有 surprised/confused。但是,如果我写

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

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

所以我的两个问题是:

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

Java 将 autobox/autounbox 执行安全的 primitive-to-primitive 转换。

这个

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

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

  • 一个用于 lambda 参数的过滤器对象,并且
  • 一个用于 isAlphabetic() 参数的 lambda 参数

允许编译。

第一个操作是将Character拆箱为char,使lambda参数成为char。第二个操作(安全地)将 char 转换为 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 Objects ,它可以接受一个 Character 实例(它允许超级引用作为 Integer ,Object 但不是 Super Instances )。您在此处尝试将实例从超类型传递到下限泛型,这会使编译器感到困惑。

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

Character::isAlphabeticc -> Character.isAlphabetic(c)的区别在于Character.isAlphabetic(int)不是重载方法,前者是exact method reference whereas the latter is an implicitly typed lambda expression.

我们可以证明接受不精确的方法引用的方式与接受隐式类型的 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.:

所指定

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.

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

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

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,而只是装箱通过过滤器后的值。