将非最终对象传递给方法引用

11
s.get()第二次返回"ONE"的解释是什么?
String x = "one";
Supplier<String> s = x::toUpperCase;
System.out.println("s.get() = " + s.get());
x = "two";
System.out.println("s.get() = " + s.get());

更新:

与之相比:

String x = "one";
Supplier<String> s = () -> x.toUpperCase();
System.out.println("s.get() = " + s.get());
x = "two";
System.out.println("s.get() = " + s.get());

它将抛出编译错误。


点赞。这比那些“因为它是一个引用,不是吗”是一个合适的答案更有趣。 - Bathsheba
@khelwood:我想说类似吧。你可以在C++中编写类似于Supplier的东西,它会输出"TWO"。 - Bathsheba
4
第二个例子不应该编译。 - Andrew Tobilko
6个回答

5

在Java中,表示对象的变量通常称为“引用(reference)”。在上面的代码中,您有两个引用xs

字符串是不可变的,并且对其进行的任何更改都表示另一个对象。一旦创建,就无法修改String对象的任何状态。

在代码中,xs都被初始化为引用2个对象,然后x被赋予引用另一个对象,但s仍然引用同一个对象。请注意,“::”会立即计算并分配结果对象。x可以独立于y更改其引用到另一个对象。

使用x =“two”只是使x引用不同的对象。


3

String是一个不可变的类,你正在执行

x = "two"; 

将对象s的值保持“完整”,与先前的值“ONE”相同。


3
只有在lambda表达式中需要传递最终变量或有效最终变量(原因)。对于以不同方式评估的方法引用,不需要这样做。

15.13.3. 方法引用的运行时评估

当一个方法引用表达式在::分隔符之前具有表达式(而不是类型)时,该子表达式将立即被求值。求值结果存储直到调用相应函数接口类型的方法;此时,结果将用作调用的目标引用。这意味着在程序遇到方法引用表达式时,仅对::分隔符之前的表达式进行求值,并且在对功能接口类型进行后续调用时不会重新求值。

因此,变量不必是final
实际上,一个类是否是不可变的并不重要。相反,左侧的方法引用是否为表达式才是重要的。
我想展示一个简短的例子来让你理解:
class A {
    public static void main(String[] args) {

        Supplier<A> supplier1 = A::new; // (1)
        Supplier<A> supplier2 = new A()::self; // (2) 

        A r1 = supplier1.get(); // (3)
        A r2 = supplier2.get(); // (4)
    }

    private A self() { return this; }

}
  1. 已创建供应商实例,结果尚未评估(使用类型的方法引用)。
  2. 已计算供应商及其结果(使用new A()表达式的方法引用)。
  3. 每次调用supplier1.get()时,都会重新评估。
  4. 将返回步骤2中的结果。

这是否意味着 toUpperCase 方法仅在“一个”实例上调用一次? - Stephen L.
1
@StephenL.,该方法将被调用2次,但结果只会在程序遇到方法引用表达式时评估一次。我在答案中进行了标记。 - Andrew Tobilko

2

这是一个有趣的问题,我运用反编译器进行了研究 - 但答案支持Andrew Tobilko的回答。

java -jar cfr_0_119.jar LambdaTest --decodelambdas false

/*
 * Decompiled with CFR 0_119.
 */
import java.io.PrintStream;
import java.lang.invoke.LambdaMetafactory;
import java.util.function.Supplier;

public class LambdaTest {
    public static void main(String[] args) {
        String x = "one";
        Supplier<String> s = (Supplier<String>)LambdaMetafactory.metafactory(null, null, null, ()Ljava/lang/Object;, toUpperCase(), ()Ljava/lang/String;)((String)x);
        System.out.println("s.get() = " + s.get());
        x = "two";
        System.out.println("s.get() = " + s.get());
    }
}

因此,方法引用获取的是 x 的第一个实例的副本,这就是为什么它会输出两次“ONE”的原因,而且不会创建静态 lambda,只会调用 toUpper。

我还运行了第二个示例,它确实创建了一个 lambda(我错过了不能编译的部分 -

java -jar cfr_0_119.jar LambdaTest --decodelambdas false
/*
 * Decompiled with CFR 0_119.
 */
import java.io.PrintStream;
import java.lang.invoke.LambdaMetafactory;
import java.util.function.Supplier;

public class LambdaTest {
    public static void main(String[] args) {
        String y = "one";
        Supplier<String> sy = (Supplier<String>)LambdaMetafactory.metafactory(null, null, null, ()Ljava/lang/Object;, lambda$0(java.lang.String ), ()Ljava/lang/String;)((String)y);
        System.out.println("sy.get() = " + sy.get());
    }

    private static /* synthetic */ String lambda$0(String string) {
        return string.toUpperCase();
    }
}

0

字符串是不可变的:

String x = "one";
Supplier<String> s = x::toUpperCase;

等同于:

String x = "one";
Supplier<String> s = "one"::toUpperCase;

-1
你创建了一个 Supplier,它只提供值,而在这种情况下,每次都是相同的值,因为此处的 x 值仅在创建 lambda 时转换一次。
你需要的是一个 Function,它接受一个参数并返回一个结果。
试试这个:
String x = "one";
Function<String, String> s = String::toUpperCase;
System.out.println("s.apply(x) = " + s.apply(x));
x = "two";
System.out.println("s.apply(x) = " + s.apply(x));

我创建了供应商是为了有一个方法引用实例的原因。 - Stephen L.
实例是一个包含文本“one”的字符串。因此,它永远不会改变,因为字符串是不可变的。你可以写成:"one"::toUpperCase。 - john16384

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