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

Lui*_*ese 32 java lambda closures java-8 method-reference

我对内部类和lambda表达有些困惑,我试着问一个问题,然后又出现了另一个疑问,并且可能更好地发布另一个问题,而不是评论前一个问题.

直截了当:我知道(谢谢Jon)这样的事情无法编译

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(); }
Run Code Online (Sandbox Code Playgroud)

由于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(); }
Run Code Online (Sandbox Code Playgroud)

//2等于//1?在第二种情况下,我是否面临"使用过时变量"的风险?

我的意思是,在后一种情况下,one = new One();执行后f仍然有一个过时的副本one(即引用旧对象).这不是我们试图避免的歧义吗?

Mar*_*eel 31

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

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

Runnable f = () -> one.bar();
Run Code Online (Sandbox Code Playgroud)

这相当于一个实现的匿名类Runnable:

Runnable f = new Runnable() {
    public void run() {
       one.bar();
    }
}
Run Code Online (Sandbox Code Playgroud)

这里适用的规则与匿名类(或方法本地类)相同.这意味着one需要有效地最终使其发挥作用.

另一方面方法句柄:

Runnable f = one::bar;
Run Code Online (Sandbox Code Playgroud)

更像是:

Runnable f = new MethodHandle(one, one.getClass().getMethod("bar"));
Run Code Online (Sandbox Code Playgroud)

随着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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • @LuigiCortese不,它是由编译器创建的合成对象,它引用要在其上调用方法的对象. (2认同)

Rea*_*tic 10

你的第二个例子不是lambda表达式.这是一个方法参考.在这种特殊情况下,它从特定对象中选择一个方法,该方法当前由变量引用one.但是参考是到对象,不将可变一个.

这与传统的Java案例相同:

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

two.bar();
Run Code Online (Sandbox Code Playgroud)

那么如果one改变了呢?two引用one以前的对象,并可以访问其方法.

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

  • 我不会诉诸魔术,真的。我认为这里发生的事情是为了避免混淆,JLS 可以明确地说“闭包中的任何本地引用都是值的副本,就像控制到达匿名类定义之前的行一样”。但是他们没有,因为他们将来可能想使用全闭包,并且不想束缚自己。因此,在下一版本的 Java 中,他们可以说“对闭包中的局部变量不再有任何限制”——所有旧代码仍然可以工作,而新代码不会绑定到旧规范。 (2认同)

Pau*_*ton 8

共识似乎是因为当您使用匿名类执行此操作时,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.
}
Run Code Online (Sandbox Code Playgroud)

在这个例子中,我们调用toStringo方法之后getStringSupplier又回来了,所以当它出现在get方法,o不能参照的局部变量getStringSupplier的方法.实际上它基本上等同于:

static Supplier<String> getStringSupplier() {
    final Object o = new Object();
    return new StringSupplier(o);
}

private static class StringSupplier implements Supplier<String> {
    private final Object o;

    StringSupplier(Object o) {
        this.o = o;
    }

    @Override
    public String get() {
        return o.toString();
    }
} 
Run Code Online (Sandbox Code Playgroud)

匿名类成功 看起来好像使用局部变量,实际上捕获了这些变量的值.

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

Lambda表达式

Lambda表达式也会关闭值,而不是变量.Brian Goetz撰写给出的理由在这里

像这样的成语:

int sum = 0;
list.forEach(e -> { sum += e.size(); }); // ERROR
Run Code Online (Sandbox Code Playgroud)

基本上是连续的; 写这样没有竞争条件的lambda体是很困难的.除非我们愿意强制执行 - 最好是在编译时 - 这样的函数无法逃避其捕获线程,否则此功能可能会导致比它解决的更多麻烦.

方法参考

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

例如,以下代码打印 "a"两次:

String s = "a";
Supplier<String> supplier = s::toString;
System.out.println(supplier.get());
s = "b";
System.out.println(supplier.get());
Run Code Online (Sandbox Code Playgroud)

摘要

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

鉴于此,问题是,为什么适用于匿名类和lambda表达式的规则不适用于方法引用,也就是说,为什么o::toStringo不能有效最终时允许写入?我不知道答案,但在我看来确实是一个不一致的地方.我想这是因为你不能用方法参考做同样多的伤害; 像上面引用的lambda表达式的示例不适用.