在 Java8 中引用具有不同参数的方法

2022-09-03 01:55:55

我想知道所有这些带有方法引用和功能接口的东西如何在较低级别上工作。最简单的例子是我们有一些列表

List<String> list = new ArrayList<>();
list.add("b");
list.add("a");
list.add("c"):

现在我们想用 Collections 类对它进行排序,这样我们就可以调用:

Collections.sort(list, String::compareToIgnoreCase);

但是,如果我们定义自定义比较器,它可能是这样的:

Comparator<String> customComp = new MyCustomOrderComparator<>();
Collections.sort(list, customComp::compare);

问题在于需要两个参数:List 和 Comparator。由于比较器是功能接口,因此可以将其替换为具有相同签名(参数和返回类型)的lambda表达式或方法引用。那么,当传递方法引用一个实例方法时,当这些方法的签名不匹配时,传递这些方法引用如何工作?如何在 Java8 中翻译方法引用?Collections.sortString::compareToIgnoreCase


答案 1

Oracle 方法参考教程中

对特定类型的任意对象的实例方法的引用

以下是对特定类型的任意对象的实例方法的引用示例:

String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);

方法引用的等效 lambda 表达式将具有形式参数列表,其中 a 和 b 是任意名称,用于更好地描述此示例。方法引用将调用方法 。String::compareToIgnoreCase(String a, String b)a.compareToIgnoreCase(b)

但是,操作员的真正含义是什么?好吧,运算符可以这样描述(来自这个SO问题):::::

方法参考可以以不同的样式获得,但它们的含义都是相同的:

  1. 静态方法 (ClassName::methodName)
  2. 特定对象的实例方法 (instanceRef::methodName)
  3. 特定对象的超方法 (super::methodName)
  4. 特定类型的任意对象的实例方法 (ClassName::methodName)
  5. 类构造函数引用 (ClassName::new)
  6. 数组构造函数引用 (TypeName[]::new)

因此,这意味着方法引用属于第二类(),这意味着它可以转换为。String::compareToIgnoreCaseinstanceRef::methodName(a, b) -> a.compareToIgnoreCase(b)

我认为,以下例子进一步说明了这一点。A 包含一个方法,该方法对两个操作数进行操作并返回 .这可以伪描述为(其中操作数为 和 )。如果您以这种方式查看它,则以下所有内容都属于该类别:Comparator<String>Stringint(a, b) ==> return intab

// Trad anonymous inner class
// Operands: o1 and o2. Return value: int
Comparator<String> cTrad = new Comparator<String>() {
    @Override
    public int compare(final String o1, final String o2) {
        return o1.compareToIgnoreCase(o2);
    }
};

// Lambda-style
// Operands: o1 and o2. Return value: int
Comparator<String> cLambda = (o1, o2) -> o1.compareToIgnoreCase(o2);

// Method-reference à la bullet #2 above. 
// The invokation can be translated to the two operands and the return value of type int. 
// The first operand is the string instance, the second operand is the method-parameter to
// to the method compareToIgnoreCase and the return value is obviously an int. This means that it
// can be translated to "instanceRef::methodName".
Comparator<String> cMethodRef = String::compareToIgnoreCase;

这个伟大的SO-answer解释了lambda函数是如何编译的。在这个答案中,Jarandinor引用了Brian Goetz优秀文档中的以下段落,该文档描述了有关lambda翻译的更多信息

我们不是生成字节码来创建实现 lambda 表达式的对象(例如调用内部类的构造函数),而是描述构造 lambda 的配方,并将实际构造委托给语言运行时。该配方被编码在调用动态指令的静态和动态参数列表中。

基本上,这意味着本机运行时决定如何转换 lambda。

布莱恩继续说道:

方法引用的处理方式与 lambda 表达式相同,只是大多数方法引用不需要解砂到新方法中;我们可以简单地为引用的方法加载一个常量方法句柄,并将其传递给元工厂。

因此,lambda被解压成一种新方法。例如:

class A {
    public void foo() {
        List<String> list = ...
        list.forEach( s -> { System.out.println(s); } );
    }
}

上面的代码将解压为如下:

class A {
    public void foo() {
        List<String> list = ...
        list.forEach( [lambda for lambda$1 as Consumer] );
    }

    static void lambda$1(String s) {
        System.out.println(s);
    }
}

但是,Brian在文档中也解释了这一点:

如果去糖化方法是实例方法,则将接收方视为第一个参数

Brian 继续解释 lambda 的剩余参数将作为参数传递给引用的方法

因此,在Moandji Ezana的这个条目的帮助下,作为a的脱糖可以分解为以下步骤:compareToIgnoreCaseComparator<String>

  • Collections#sort对于一个期望List<String>Comparator<String>
  • Comparator<String>是与方法的函数接口,它等价于int sort(String, String)BiFunction<String, String, Integer>
  • 因此,比较器实例可以由兼容的 lambda 提供:BiFunction(String a, String b) -> a.compareToIgnoreCase(b)
  • String::compareToIgnoreCase引用一个接受参数的实例方法,因此它与上面的lambda兼容:成为接收方并成为方法参数StringString aString b

编辑:在OP输入之后,我添加了一个低级示例来解释脱糖


答案 2

推荐