参考 Java8 中不同参数的方法

Reference to methods with different parameters in Java8

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

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

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

来自Oracle method references tutorial

Reference to an Instance Method of an Arbitrary Object of a Particular Type

The following is an example of a reference to an instance method of an arbitrary object of a particular type:

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

The equivalent lambda expression for the method reference String::compareToIgnoreCase would have the formal parameter list (String a, String b), where a and b are arbitrary names used to better describe this example. The method reference would invoke the method a.compareToIgnoreCase(b).

但是,:: 运算符的真正含义是什么?那么,:: 运算符可以这样描述(来自 this SO question):

Method Reference can be obtained in different styles, but they all mean the same:

  1. A static method (ClassName::methodName)
  2. An instance method of a particular object (instanceRef::methodName)
  3. A super method of a particular object (super::methodName)
  4. An instance method of an arbitrary object of a particular type (ClassName::methodName)
  5. A class constructor reference (ClassName::new)
  6. An array constructor reference (TypeName[]::new)

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

我相信以下示例可以进一步说明这一点。 Comparator<String> 包含一个对两个 String 操作数和 returns 一个 int 操作的方法。这可以伪描述为 (a, b) ==> return int(其中操作数是 ab)。如果您以这种方式查看,则以下所有内容都属于该类别:

// 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;

This great SO-answer to explains how lambda functions are compiled. In that answer Jarandinor refers to the following passage from Brian Goetz excellent document that describes more about lambda translations.

Instead of generating bytecode to create the object that implements the lambda expression (such as calling a constructor for an inner class), we describe a recipe for constructing the lambda, and delegate the actual construction to the language runtime. That recipe is encoded in the static and dynamic argument lists of an invokedynamic instruction.

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

布赖恩继续说:

Method references are treated the same way as lambda expressions, except that most method references do not need to be desugared into a new method; we can simply load a constant method handle for the referenced method and pass that to the metafactory.

因此,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 as Consumer] );
    }

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

但是,Brian 在文档中也对此进行了解释:

if the desugared method is an instance method, the receiver is considered to be the first argument

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

因此,在this entry by Moandji Ezana的帮助下,compareToIgnoreCase作为Comparator<String>的脱糖可以分解为以下步骤:

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

编辑: 在从 OP 输入后,我添加了一个 low level 示例来解释 desugaring