ChatGPT解决这个技术问题 Extra ChatGPT

为什么在匿名类中只能访问最终变量?

在这里只能是最终的。为什么?如何在不将其保留为私有成员的情况下重新分配 onClick() 方法? private void f(Button b, final int a){ b.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { int b = a*5; } });点击时如何返回 5 * a?我的意思是,private void f(Button b, final int a){ b.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { int b = a*5; return b; // 但返回类型是空白 } }); }

我不认为 Java 匿名类提供了你所期望的那种 lambda 闭包,但是如果我错了,请有人纠正我......
你想达到什么目的?当“f”完成时,可以执行点击处理程序。
@Lambert 如果你想在 onClick 方法中使用 a 它必须是最终的 @Ivan f() 方法如何表现得像 onClick() 方法在单击时返回 int
这就是我的意思——它不支持完全闭包,因为它不允许访问非最终变量。
注意:从 Java 8 开始,您的变量只需要有效地为 final

J
Jon Skeet

正如评论中所指出的,其中一些在 Java 8 中变得无关紧要,其中 final 可以是隐式的。不过,只有 有效 final 变量可以用于匿名内部类或 lambda 表达式。

这基本上是由于 Java 管理 closures 的方式。

当您创建匿名内部类的实例时,该类中使用的任何变量都会通过自动生成的构造函数复制其值。这避免了编译器必须自动生成各种额外的类型来保存“局部变量”的逻辑状态,例如 C# 编译器......(当 C# 在匿名函数中捕获变量时,它确实捕获了变量 -闭包可以以方法主体看到的方式更新变量,反之亦然。)

由于该值已被复制到匿名内部类的实例中,因此如果该变量可以被该方法的其余部分修改,那将看起来很奇怪——您可能拥有似乎正在使用过期变量的代码(因为这实际上就是会发生的事情……您将使用在不同时间拍摄的副本)。同样,如果您可以在匿名内部类中进行更改,开发人员可能希望这些更改在封闭方法的主体中可见。

将变量设为 final 消除了所有这些可能性——因为值根本无法更改,您无需担心此类更改是否可见。允许方法和匿名内部类看到彼此更改的唯一方法是使用某种描述的可变类型。这可能是封闭类本身、一个数组、一个可变包装器类型......任何类似的东西。基本上,这有点像一种方法和另一种方法之间的通信:调用者看不到对一种方法的参数所做的更改,但可以看到对参数引用的对象所做的更改。

如果您对 Java 和 C# 闭包之间的更详细比较感兴趣,我有一个 article 可以进一步讨论。我想在这个答案中专注于 Java 方面:)


是的。基本上,可以通过移动在特殊自动生成的类中引用的所有变量来实现完全闭包支持。
@Ivan:基本上就像 C#。但是,它具有相当程度的复杂性,如果您想要与 C# 相同的功能,其中来自不同范围的变量可以“实例化”不同次数。
这对于 Java 7 来说都是正确的,请记住,在 Java 8 中引入了闭包,现在确实可以从内部类访问类的非 final 字段。
@MathiasBader:真的吗?我认为它本质上仍然是相同的机制,编译器现在足够聪明地推断 final (但它仍然需要有效地最终)。
@Mathias Bader:您始终可以访问非最终字段,这些字段不要与局部变量混淆,局部变量必须是最终的并且仍然必须是有效的最终,因此 Java 8 不会改变语义。
A
Andrii Abramov

有一个技巧可以让匿名类在外部范围内更新数据。

private void f(Button b, final int a) {
    final int[] res = new int[1];
    b.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
            res[0] = a * 5;
        }
    });

    // But at this point handler is most likely not executed yet!
    // How should we now res[0] is ready?
}

但是,由于同步问题,这个技巧不是很好。如果稍后调用处理程序,则需要 1) 如果从不同线程调用处理程序,则同步对 res 的访问 2) 需要具有某种标志或指示 res 已更新

但是,如果立即在同一线程中调用匿名类,则此技巧可以正常工作。喜欢:

// ...

final int[] res = new int[1];
Runnable r = new Runnable() { public void run() { res[0] = 123; } };
r.run();
System.out.println(res[0]);

// ...

感谢您的回答。我知道这一切,我的解决方案比这更好。我的问题是“为什么只有决赛”?
那么答案就是它们的实现方式:)
谢谢。我自己使用了上面的技巧。我不确定这是否是个好主意。如果 Java 不允许这样做,可能有充分的理由。您的回答表明我的 List.forEach 代码是安全的。
阅读 stackoverflow.com/q/12830611/2073130,详细讨论“为什么只有 final”背后的基本原理。
有几种解决方法。我的是:final int resf = res;最初我使用数组方法,但我发现它的语法过于繁琐。 AtomicReference 可能会慢一些(分配一个对象)。
C
Community

匿名类是内部类,严格规则适用于内部类 (JLS 8.1.3)

任何使用但未在内部类中声明的局部变量、形式方法参数或异常处理程序参数都必须声明为 final。任何在内部类中使用但未声明的局部变量必须在内部类的主体之前明确分配。

我还没有找到关于 jls 或 jvms 的原因或解释,但我们知道,编译器为每个内部类创建一个单独的类文件,它必须确保在这个类文件上声明的方法(在字节码级别)至少可以访问局部变量的值。

Jon has the complete answer - 我保留这个不删除,因为有人可能对 JLS 规则感兴趣)


A
Andrii Abramov

您可以创建一个类级别的变量来获取返回值。我是说

class A {
    int k = 0;
    private void f(Button b, int a){
        b.addClickHandler(new ClickHandler() {
        @Override
        public void onClick(ClickEvent event) {
            k = a * 5;
        }
    });
}

现在你可以得到 K 的值并在你想要的地方使用它。

你为什么的答案是:

本地内部类实例与 Main 类绑定,并且可以访问其包含方法的最终局部变量。当实例使用其包含方法的 final 局部变量时,该变量将保留它在实例创建时所持有的值,即使该变量已超出范围(这实际上是 Java 粗略的、有限的闭包版本)。

因为本地内部类既不是类的成员也不是包的成员,所以它没有用访问级别声明。 (但是要清楚,它自己的成员具有与普通类一样的访问级别。)


我提到“没有将其保留为私人成员”
先生,您能否简单地说一下“即使变量超出范围”是什么意思?
c
cfy

要了解此限制的基本原理,请考虑以下程序:

public class Program {

    interface Interface {
        public void printInteger();
    }
    static Interface interfaceInstance = null;

    static void initialize(int val) {
        class Impl implements Interface {
            @Override
            public void printInteger() {
                System.out.println(val);
            }
        }
        interfaceInstance = new Impl();
    }

    public static void main(String[] args) {
        initialize(12345);
        interfaceInstance.printInteger();
    }
}

在initialize方法返回后interfaceInstance保留在内存中,但参数val没有。 JVM 无法访问其范围之外的局部变量,因此 Java 通过将 val 的值复制到 interfaceInstance 中的同名隐式字段来使后续调用 printInteger 工作。据说 interfaceInstance 已经捕获了本地参数的值。如果参数不是最终的(或实际上是最终的),它的值可能会改变,与捕获的值不同步,可能会导致不直观的行为。


Z
Zach L

好吧,在 Java 中,一个变量不仅可以作为参数,还可以作为类级别的字段,例如

public class Test
{
 public final int a = 3;

或作为局部变量,例如

public static void main(String[] args)
{
 final int a = 3;

如果要访问和修改来自匿名类的变量,您可能希望使该变量成为封闭类中的类级别变量。

public class Test
{
 public int a;
 public void doSomething()
 {
  Runnable runnable =
   new Runnable()
   {
    public void run()
    {
     System.out.println(a);
     a = a+1;
    }
   };
 }
}

您不能将变量作为 final 并且 给它一个新值。 final 意味着:该值是不可更改的和最终的。

而且由于它是最终的,Java 可以安全地将其复制到本地匿名类中。您没有获得对 int 的一些引用(特别是因为您不能引用 Java 中的 int 等原语,而只能引用 Objects)。

它只是将 a 的值复制到匿名类中称为 a 的隐式 int 中。


我将“类级变量”与 static 相关联。如果您改用“实例变量”,可能会更清楚。
好吧,我使用了类级别,因为该技术适用于实例变量和静态变量。
我们已经知道 final 是可以访问的,但我们想知道为什么?您能否在为什么方面添加更多解释?
S
Swapnil Gangrade

访问仅限于局部最终变量的原因是,如果所有局部变量都可以访问,那么首先需要将它们复制到内部类可以访问它们并维护多个副本的单独部分可变局部变量可能导致数据不一致。而最终变量是不可变的,因此对它们的任何数量的副本都不会对数据的一致性产生任何影响。


这不是在支持此功能的 C# 等语言中实现的方式。实际上,编译器将变量从局部变量更改为实例变量,或者它为这些变量创建了一个额外的数据结构,可以超出外部类的范围。但是,没有“局部变量的多个副本”
Mike76 我没有看过 C# 的实现,但是 Scala 做了你提到的第二件事,我认为:如果 Int 被重新分配到闭包内,则将该变量更改为 IntRef 的实例(本质上是一个可变的Integer 包装器)。然后相应地重写每个变量访问。
o
ola_star

当在方法体中定义匿名内部类时,在该方法范围内声明为 final 的所有变量都可以从内部类中访问。对于标量值,一旦被赋值,最终变量的值就不能改变。对于对象值,引用不能更改。这允许 Java 编译器在运行时“捕获”变量的值并将副本作为字段存储在内部类中。一旦外部方法终止并且它的堆栈帧被删除,原始变量就消失了,但内部类的私有副本仍然存在于类自己的内存中。

(http://en.wikipedia.org/wiki/Final_%28Java%29)


S
S73417H

异常内部类中的方法可能会在产生它的线程终止后被调用。在您的示例中,内部类将在事件分派线程上调用,而不是在与创建它的线程相同的线程中。因此,变量的范围会有所不同。因此,为了保护此类变量分配范围问题,您必须将它们声明为 final。


B
Bo Persson
private void f(Button b, final int a[]) {

    b.addClickHandler(new ClickHandler() {

        @Override
        public void onClick(ClickEvent event) {
            a[0] = a[0] * 5;

        }
    });
}

m
mathk

由于 Jon 有实现细节的答案,另一个可能的答案是 JVM 不想处理已结束其激活的写入记录。

考虑将您的 lambdas 存储在某个地方并稍后运行的用例,而不是被应用。

我记得在 Smalltalk 中,当您进行此类修改时,您会遇到非法商店。


J
Jouby

试试这个代码,

创建数组列表并将值放入其中并返回:

private ArrayList f(Button b, final int a)
{
    final ArrayList al = new ArrayList();
    b.addClickHandler(new ClickHandler() {

         @Override
        public void onClick(ClickEvent event) {
             int b = a*5;
             al.add(b);
        }
    });
    return al;
}

OP正在询问为什么需要某些东西的原因。因此,您应该指出您的代码如何解决它
S
Sam YC

Java 匿名类与 Javascript 闭包非常相似,但 Java 以不同的方式实现它。 (检查安徒生的答案)

因此,为了不让 Java 开发人员与来自 Javascript 背景的人可能发生的奇怪行为混淆。我想这就是他们强迫我们使用 final 的原因,这不是 JVM 限制。

让我们看一下下面的 Javascript 示例:

var add = (function () {
  var counter = 0;

  var func = function () {
    console.log("counter now = " + counter);
    counter += 1; 
  };

  counter = 100; // line 1, this one need to be final in Java

  return func;

})();


add(); // this will print out 100 in Javascript but 0 in Java

在 Javascript 中,counter 的值为 100,因为从头到尾只有一个 counter 变量。

但是在 Java 中,如果没有 final,它会打印出 0,因为在创建内部对象时,0 值被复制到内部类对象的隐藏属性中。 (这里有两个整数变量,一个在本地方法中,另一个在内部类隐藏属性中)

因此内部对象创建后的任何更改(如第 1 行)都不会影响内部对象。因此,它会混淆两种不同的结果和行为(Java 和 Javascript 之间)。

我相信这就是为什么 Java 决定强制它成为最终的,所以数据从头到尾都是“一致的”。


y
yoAlex5

inner class[About] 中的 Java final 变量

内部类只能使用

来自外部类的引用 来自范围外的最终局部变量是引用类型(例如 Object...) 值(原始)(例如 int...)类型可以由最终引用类型包装。 IntelliJ IDEA 可以帮你把它转换成一个元素数组

当编译器生成 non static nested (inner class) - 创建一个新类 - <OuterClass>$<InnerClass>.class 并将 bounded 参数传递给 constructor[Local variable on stack] 它是类似于关闭[Swift about]

final 变量是一个不能重新赋值的变量。最终引用变量仍然可以通过修改状态来更改

如果有可能,那会很奇怪,因为作为程序员,您可以像这样

//Not possible 
private void foo() {

    MyClass myClass = new MyClass(); //Case 1: myClass address is 1
    int a = 5;                       //Case 2: a = 5

    //just as an example
    new Button().addClickHandler(new ClickHandler() {
        
        @Override
        public void onClick(ClickEvent event) {

            /*
            myClass.something(); //<- what is the address - 1 or 2?
            int b = a; //<- what is the value - 5 or 10 ?

            //illusion that next changes are visible for Outer class
            myClass = new MyClass();
            a = 15;
            */
        }
    });

    myClass = new MyClass(); //Case 1: myClass address is 2
    int a = 10; //Case 2: a = 10
}

W
Whimusical

也许这个技巧给了你一个想法

Boolean var= new anonymousClass(){
    private String myVar; //String for example
    @Overriden public Boolean method(int i){
          //use myVar and i
    }
    public String setVar(String var){myVar=var; return this;} //Returns self instane
}.setVar("Hello").method(3);