Java方法引用解析

5
我是一个有用的助手,可以翻译文本。
我正在尝试理解Java中的方法引用如何工作。 乍一看,它非常简单。但是当涉及到这样的事情时就不是那么简单了:
Foo类中有一个方法:
public class Foo {
    public Foo merge(Foo another) {
        //some logic
    }
}

在另一个类 Bar 中,有一个像这样的方法:
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);

???


3
这是在类Foo中的一个方法吗?如果是的话,那么它将是(this, another) BiFunction。 - Elazar
4
如果merge(...)Foo类的实例方法,那么它有一个隐含的this参数。 - Turing85
1
我可以像对待 Foo merge(Foo this, Foo that) 一样对待 Foo merge(Foo another) 吗?这并不明显。 - user1679671
1
@KirillBazarov 乍一看似乎不是这样。但如果你知道方法调用的实际工作方式以及方法存储的位置,那么很明显每个实例方法都有一个隐式的 this 参数。编写 someObject.doSomethingWith(someOtherObject)doSomethingWith(someObject, someOtherObject) 的一种方便(且更具认知吸引力)的替代方法。 - Turing85
2
也被称为“特定类型任意对象的实例方法引用”。 - Eugene
1
@KirillBazarov 不是你会把 Foo merge(Foo another) 看成 Foo merge(Foo this, Foo another)。实际上,就是这么回事。你甚至可以声明你的 merge 方法以接收 this,它也能正常编译。只不过谁会显式地声明一个不必要的隐式 this 参数呢... 但是底层它就是这样工作的。 - fps
2个回答

3

实例方法中有一个隐式的this参数。这在JVM规范的§3.7中定义:

首先将当前实例的引用this推送到操作数栈上来设置调用。然后,将方法调用的参数int值12和13推入栈中。创建addTwo方法的帧时,传递给该方法的参数成为新帧局部变量的初始值。也就是说,调用者推送到操作数栈上的this引用和两个参数将成为被调用方法的局部变量0、1和2的初始值。

为了理解为什么方法调用会以这种方式进行,我们需要了解JVM如何在内存中存储代码。代码和对象的数据是分开的。事实上,一个类的所有方法(静态和非静态)都存储在同一个地方,即方法区(JVM规范§2.5.4)。这样可以将每个方法仅存储一次,而不是为类的每个实例反复重新存储它们。当像
someObject.doSomethingWith(someOtherObject);

被称为时,它实际上编译成了更接近以下内容的东西

doSomething(someObject, someOtherObject);

大多数Java程序员都会认为someObject.doSomethingWith(someOtherObject)具有“较低的认知复杂度”:我们对涉及someOtherObjectsomeObject进行操作。这个动作的中心是someObject,而someOtherObject只是达到目的的手段。
使用doSomethingWith(someObject, someOtherObject),您无法传递someObject是行动中心的语义。
因此,实质上,我们编写了第一个版本,但计算机更喜欢第二个版本。
正如@FedericoPeraltaSchaffner所指出的那样,自Java 8以来,甚至可以明确地编写隐式的this参数。JLS,§8.4.1给出了确切的定义:
接收器参数是实例方法或内部类构造函数的可选语法设备。对于实例方法,接收器参数表示调用该方法的对象。对于内部类的构造函数,接收器参数表示新构造对象的直接封闭实例。无论哪种方式,接收器参数仅存在于允许在源代码中表示所代表对象类型以进行注释的目的,因此可以注解类型。接收器参数不是正式参数;更准确地说,它不是任何变量的声明(§4.12.3),它从未绑定到在方法调用表达式或限定类实例创建表达式中传递的任何值,并且在运行时根本没有任何影响。

接收器参数必须是类的类型,并且必须命名为this

这意味着

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

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

这两者具有相同的语义意义,但后者允许进行注释等操作。


非常有帮助的解释。谢谢。 - user1679671

2

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

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. 方法引用的运行时评估

如果形式是ReferenceType :: [TypeArguments] Identifier
[...]
如果编译时声明是实例方法,则目标引用是调用方法的第一个形式参数。否则,没有目标引用。

我们可以在以下主题中找到一些使用此行为的示例:
JLS - 15.13.1. 方法引用的编译时声明提到:

形式为ReferenceType :: [TypeArguments] Identifier的方法引用表达式可以有不同的解释方式。
- 如果Identifier引用实例方法,则隐式lambda表达式具有额外的参数[...]
- 如果Identifier引用静态方法。 ReferenceType可能同时具有两种适用的方法,因此上述搜索算法会分别识别它们,因为每种情况的参数类型不同。

然后它展示了这种行为可能存在的一些歧义:
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)?
    }
}

如果我的理解正确的话,我相信15.13.3已经解释了它。然后15.13.1添加了ReferenceType :: [TypeArguments] identifier语法的两种解释。请确认或否认我的理解(我有所怀疑)。 - AxelH

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接