【问题标题】:Isn't effectively final/final restriction useless? [duplicate]有效的最终/最终限制不是无用的吗? [复制]
【发布时间】:2018-05-25 12:32:14
【问题描述】:

我不明白为什么 java 语言的实现者会这样做,以便在 lambda 中使用并从函数范围传递的变量必须是最终的。

我反编译了这段代码:

public class Main {
    @FunctionalInterface
    interface Test {
        void method(int t);
    }

    static void test(Test t) {
        t.method(3);
    }

    public static void main(String... args) {
        int a = 3;
        test((i)-> {
            System.out.println(a + i);
        });
    }
}

编译器所做的就是复制该变量,就好像它是通过构造函数传递的一样。我有这 3 个课程:

1:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import Main.1;
import Main.Test;

public class Main {
    public Main() {
    }

    static void test(Test var0) {
        var0.method(3);
    }

    public static void main(String... var0) {
        byte var1 = 3;
        test(new 1(var1));
    }
}

2:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import Main.Test;

final class Main$1 implements Test {
    Main$1(int var1) {
        this.val$a = var1;
    }

    public void method(int var1) {
        System.out.println(this.val$a + var1);
    }
}

3:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

@FunctionalInterface
interface Main$Test {
    void method(int var1);
}

为什么实现者不能只复制变量而不管它是否被修改,所以我们可以这样做:

public class Main {
    @FunctionalInterface
    interface Test {
        void method(int t);
    }

    static void test(Test t) {
        t.method(3);
    }

    public static void main(String... args) {
        int a = 3;
        test((i)-> {
            a += 1; // allow modification, a is copied anyway, why not?
            System.out.println(a + i);
        });
    }
}

【问题讨论】:

  • 原因是为了简化代码,防止并发的复杂性。您声明了一个在 lambda 中使用的值,该值可以随时执行,难以遵循,因此强制它为 final 是一个不错的选择。这并不新鲜,顺便说一下,匿名内部类已经是这种情况了。实际上 final 只是一个语法糖(我认为没有必要),因为它的风格最终需要是 final 的。因此,将其声明为 final 并防止更新期间出现任何复杂情况会更简单。

标签: java jvm


【解决方案1】:

这没有技术原因。只是如果您允许在 lambda 中使用非最终字段,那么您可以编写看起来不错但实际上不起作用的代码。

例如:

void printSum(Collection<Integer> numbers) {
  int sum = 0;
  numbers.forEach(i -> sum += i);
  System.out.println(sum);
}

目前编译器不允许您这样做,因为您无法访问 lambda 中的非最终 sum。正如您所指出的,变量无论如何都会被复制到 lambda 中,所以它可以允许这样做。

如果是这样,那么这段代码会编译,但总是打印 0,因为只有 lambda 中的 sum-copy 被修改,而不是“真实的”。

只允许引用“有效最终”变量是一个很好的折衷方案,它不需要在任何地方都使用 final 关键字,同时仍然避免这样的误解。

【讨论】:

  • @POrekhov:它不起作用,因为 lambda 中的 sum 实际上是方法中 sum 的副本(正如您已经证明的那样)并且更改为 lambda 自己的副本sum 不会传播回原始方法。因此,如果您在原始方法中访问 sum 后在 lambda 中对其进行了修改,那么您会失望地注意到它的值仍然是 0。
  • @POrekhov:我想我明白了。如果您允许修改 lambda 中的变量并且 不要 将其传播回来(这很容易实现),那么这将允许这样的代码编译,如果没有任何明显的诊断消息将无法工作用户知道什么是错的。完全禁止修改“共享”变量会使这个问题消失而没有任何重大缺陷。
  • @POrekhov:这样看:如果您允许修改变量而不将值传播回“原始”,那么即使您只定义了一个。这令人困惑且不明显,因此他们决定不这样做。
  • @POrekhov:我不同意这里的“不问问题”部分。有一个原因,这更多是关于代码及其行为的可理解性和简单性,而不是强大的技术原因。
  • 请注意,有 三个 sum 变量。周围上下文的局部变量、生成类的final 实例字段和持有lambda 主体的合成方法的参数变量。允许更改 lambda 主体中的 sum 只会更改合成方法的参数变量,甚至不会更改保存捕获值的实例字段。所以sum 将再次是0 对于集合的每个元素。这比不将其传播回周围的上下文更不直观。
猜你喜欢
  • 2014-02-23
  • 2011-03-14
  • 2018-04-18
  • 2018-09-15
  • 2020-12-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-20
相关资源
最近更新 更多