Java 方法参考解析

Java method reference resolving

我正在尝试了解方法引用在 java 中的工作原理。 乍一看,它非常简单。但当涉及到这些事情时就不是这样了:

Foo中有一个方法class:

public class Foo {
    public Foo merge(Foo another) {
        //some logic
    }
}

在另一个class栏中有这样的方法:

public class Bar {
    public void function(BiFunction<Foo, Foo, Foo> biFunction) {
       //some logic
    }
}

并使用方法参考:

new Bar().function(Foo::merge);

它符合并有效,但我不明白它是如何匹配的:

Foo merge(Foo another)

到 BiFunction 方法:

R apply(T t, U u);

???

我发现使用不同的类型更容易理解 :

public class A {

    public void test(){
        function(A::merge);
    }

    public void function(BiFunction<A, B, C> f){

    }

    public C merge(B i){
        return null;
    }

    class B{}
    class C{}
}

我们可以知道,使用方法引用 Test::merge 而不是实例上的引用将隐式使用 this 作为第一个值。


15.13.3. Run-Time Evaluation of Method References

If the form is ReferenceType :: [TypeArguments] Identifier
[...]
If the compile-time declaration is an instance method, then the target reference is the first formal parameter of the invocation method. Otherwise, there is no target reference.

我们可以在以下主题中找到使用此行为的一些示例:
JLS - 15.13.1. Compile-Time Declaration of a Method Reference 提及:

A method reference expression of the form ReferenceType::[TypeArguments] Identifier can be interpreted in different ways.
- If Identifier refers to an instance method, then the implicit lambda expression has an extra parameter [...]
- if Identifier refers to a static method. It is possible for ReferenceType to have both kinds of applicable methods, so the search algorithm described above identifies them separately, since there are different parameter types for each case.

然后显示此行为可能存在一些歧义:

class C {
    int size() { return 0; }
    static int size(Object arg) { return 0; }

    void test() {
        Fun<C, Integer> f1 = C::size;
          // Error: instance method size() 
          // or static method size(Object)?
    }
}

实例方法有一个隐含的 this 参数。这是定义 §3.7 of the JVM specification:

The invocation is set up by first pushing a reference to the current instance, this, on to the operand stack. The method invocation's arguments, int values 12 and 13, are then pushed. When the frame for the addTwo method is created, the arguments passed to the method become the initial values of the new frame's local variables. That is, the reference for this and the two arguments, pushed onto the operand stack by the invoker, will become the initial values of local variables 0, 1, and 2 of the invoked method.

要理解为什么方法调用是这样进行的,我们需要理解JVM是如何在内存中存储代码的。对象的代码和数据是分开的。事实上,一个class(静态和非静态)的所有方法都存储在同一个地方,即method area (§2.5.4 of JVM spec)。这允许每个方法只存储一次,而不是一遍又一遍地为 class 的每个实例重新存储它们。当像

这样的方法时
someObject.doSomethingWith(someOtherObject);

被调用,它实际上被编译成看起来更像

的东西
doSomething(someObject, someOtherObject);

大多数 Java-程序员会同意 someObject.doSomethingWith(someOtherObject) 有一个 "lower cognitive complexity":我们用 someObject 做一些涉及 someOtherObject 的事情。此操作的中心是 someObject,其中 someOtherObject 只是达到目的的手段。

使用 doSomethingWith(someObject, someOtherObject),您不会传递 someObject 作为动作中心的这种语义。

所以本质上,我们写的是第一个版本,但是计算机更喜欢第二个版本。

正如@FedericoPeraltaSchaffner 所指出的,您甚至可以显式地编写隐式 this 参数,因为 Java 8。确切的定义在 JLS, §8.4.1 中给出:

The receiver parameter is an optional syntactic device for an instance method or an inner class's constructor. For an instance method, the receiver parameter represents the object for which the method is invoked. For an inner class's constructor, the receiver parameter represents the immediately enclosing instance of the newly constructed object. Either way, the receiver parameter exists solely to allow the type of the represented object to be denoted in source code, so that the type may be annotated. The receiver parameter is not a formal parameter; more precisely, it is not a declaration of any kind of variable (§4.12.3), it is never bound to any value passed as an argument in a method invocation expression or qualified class instance creation expression, and it has no effect whatsoever at run time.

接收参数必须是 class 类型并且必须命名为 this.

这意味着

public String doSomethingWith(SomeOtherClass other) { ... }

public String doSomethingWith(SomeClass this, SomeOtherClass other) { ... }

将具有相同的语义,但后者允许例如注释。