Java 为什么方法引用可以使用非最终变量?

Java 为什么方法引用可以使用非最终变量?,java,lambda,java-8,closures,method-reference,Java,Lambda,Java 8,Closures,Method Reference,我对内部类和lambda表达式有一些困惑,我试图问一个关于这个的问题,但随后又出现了另一个疑问,可能发布另一个问题比评论前一个问题更好 直截了当地说:我知道()像这样的东西是无法编译的 public class Main { public static void main(String[] args) { One one = new One(); F f = new F(){ //1 public void foo(){

我对内部类和lambda表达式有一些困惑,我试图问一个关于这个的问题,但随后又出现了另一个疑问,可能发布另一个问题比评论前一个问题更好

直截了当地说:我知道()像这样的东西是无法编译的

public class Main {
    public static void main(String[] args) {
        One one = new One();

        F f = new F(){      //1
            public void foo(){one.bar();}   //compilation error
        };

        one = new One();
    }
}

class One { void bar() {} }
interface F { void foo(); }
由于Java如何管理闭包,因为
one
不是[有效地]最终的,等等

但是,为什么允许这样做呢

public class Main {
    public static void main(String[] args) {
        One one = new One();

        F f = one::bar; //2

        one = new One();
    }
}

class One { void bar() {} }
interface F { void foo(); }
//2
是否等同于
//1
?在第二种情况下,我是否面临“使用过时变量”的风险


我的意思是,在后一种情况下,在
one=newone()之后时,
f
仍有一个过期的
副本(即引用旧对象)。这不是我们试图避免的歧义吗?

否。在第一个示例中,您定义了F inline的实现,并尝试访问实例变量one

在第二个示例中,基本上将lambda表达式定义为对对象1的
bar()
的调用

现在这可能有点混乱。这种表示法的好处是,您可以定义一次方法(大多数情况下,它是静态方法或在静态上下文中),然后从各种lambda表达式中引用相同的方法:

msg -> System.out::println(msg);

方法引用不是lambda表达式,尽管它们可以以相同的方式使用。我认为这就是造成混乱的原因。下面是Java工作原理的简化,不是它真正的工作原理,而是它已经足够接近了

假设我们有一个lambda表达式:

Runnable f = () -> one.bar();
这相当于实现
Runnable
的匿名类:

Runnable f = new Runnable() {
    public void run() {
       one.bar();
    }
}
这里的规则与匿名类(或方法本地类)相同。这意味着,
one
需要有效地完成,它才能工作

另一方面,方法处理:

Runnable f = one::bar;
更像是:

Runnable f = new MethodHandle(one, one.getClass().getMethod("bar"));
MethodHandle
为:

public class MethodHandle implements Runnable {
    private final Object object;
    private final Method method;

    public MethodHandle(Object object, java.lang.reflect.Method method) {
        this.object = Object;
        this.method = method;
    }

    @Override
    public void run() {
        method.invoke(object);
    }
}

在这种情况下,分配给
one
的对象被分配为创建的方法句柄的一部分,因此
one
本身不需要是有效的最终对象,这样才能工作。

您的第二个示例根本不是lambda表达式。这是一个方法参考。在这种特殊情况下,它从特定对象中选择一种方法,该对象当前由变量
one
引用。但是引用的是对象,而不是变量

这与经典Java案例相同:

One one = new One();
One two = one;
one = new One();

two.bar();
那么如果
one
发生了变化呢
two
引用了
one
以前的对象,并且可以访问其方法


另一方面,您的第一个示例是一个匿名类,它是一个经典的Java结构,可以引用它周围的局部变量。代码指的是实际变量
one
,而不是它所指的对象。这是由于Jon在您提到的答案中提到的原因而受到限制的。注意,Java8中的变化仅仅是变量必须是有效的final。也就是说,初始化后仍然无法更改。编译器变得足够复杂,可以确定哪种情况不会混淆,即使未明确使用
final
修饰符。

共识似乎是,这是因为当使用匿名类时,
one
引用变量,而当使用方法引用时,创建方法句柄时,将捕获
one
的值。事实上,我认为在这两种情况下,
one
都是一个值而不是一个变量。让我们更详细地考虑匿名类、lambda表达式和方法引用。

匿名类

考虑以下示例:

static Supplier<String> getStringSupplier() {
    final Object o = new Object();
    return new Supplier<String>() {
        @Override
        public String get() {
            return o.toString();
        }
    };
}

public static void main(String[] args) {
    Supplier<String> supplier = getStringSupplier();
    System.out.println(supplier.get());  // Use o after the getStringSupplier method returned.
}
匿名类使您看起来好像在使用局部变量,而实际上这些变量的值是被捕获的

与此相反,如果匿名类的方法引用封闭实例的字段,则不会捕获这些字段的值,并且匿名类的实例不包含对这些字段的引用;相反,匿名类持有对封闭实例的引用,并可以访问其字段(直接访问或通过合成访问器访问,具体取决于可见性)。一个优点是只需要对一个对象(而不是多个对象)进行额外的引用

Lambda表达式

Lambda表达式也覆盖值,而不是变量。Brian Goetz给出的理由是

像这样的成语:

int sum = 0;
list.forEach(e -> { sum += e.size(); }); // ERROR
基本上是连续的;写lambda体是相当困难的 像这样没有比赛条件。除非我们愿意 强制(最好是在编译时)这样的函数不能 摆脱它的捕获线程,这个特性可能会引起更多的麻烦 而不是解决问题

方法引用

方法引用在创建方法句柄时捕获变量的值这一事实很容易检查

例如,以下代码打印两次
“a”

String s = "a";
Supplier<String> supplier = s::toString;
System.out.println(supplier.get());
s = "b";
System.out.println(supplier.get());
String s=“a”;
供应商=s::toString;
System.out.println(supplier.get());
s=“b”;
System.out.println(supplier.get());
摘要

因此,总而言之,lambda表达式和方法引用只关注值,而不是变量。对于局部变量,匿名类也会关闭值。在字段的情况下,情况更加复杂,但行为基本上与捕获值相同,因为字段必须是有效的最终字段

鉴于此,问题是,为什么适用于匿名类和lambda表达式的规则不适用于方法引用,即当
o
不是有效的最终值时,为什么允许您编写
o::toString
?我不知道答案
String s = "a";
Supplier<String> supplier = s::toString;
System.out.println(supplier.get());
s = "b";
System.out.println(supplier.get());