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

388
  1. a在这里只能是final。为什么?如何在onClick()方法中重新分配a而不保留它作为私有成员变量?

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; // but return type is void 
        }
    });
}

1
我认为Java匿名类并没有提供你所期望的Lambda闭包,但如果我错了,请有人纠正我... - user541686
5
你想要实现什么?当“f”完成时,点击处理程序可以被执行。 - Ivan Dubrov
2
这就是我的意思 - 它不支持完全闭包,因为它不允许访问非 final 变量。 - user541686
4
从Java 8开始,你的变量只需要是 有效地不可变的 - Peter Lawrey
显示剩余3条评论
16个回答

526

正如评论中所述,在Java 8中,一些内容变得不再相关,因为final可以是隐式的。然而,只有“有效”(effectively)成为final的变量才能在匿名内部类或Lambda表达式中使用。


这主要是由于Java处理闭包的方式。

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

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

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

如果你对Java和C#闭包之间进行更详细的比较感兴趣,我有一篇文章对此进行了更深入的探讨。在这篇答案中,我想专注于Java方面 :)


4
像 C# 一样,基本上是这样的。不过,如果你想要与 C# 相同类型的功能,即来自不同作用域的变量可以被实例化多次,那么它会带有相当程度的复杂性。 - Jon Skeet
3
如何将 final 变量传递给匿名类? - Ustaman Sangat
12
这一切都适用于Java 7,需要注意的是,Java 8引入了闭包,现在确实可以从内部类访问类的非final字段。 - Mathias Bader
24
真的吗?我认为本质上机制仍然是一样的,只是编译器现在足够聪明,能够推断出final(但它仍然需要是有效的final)。 - Thilo
4
你可以随时访问非最终的“字段”(fields),这些字段不应与必须是final且仍然必须是有效final的“局部”变量混淆,所以Java 8并没有改变语义。 - Holger
显示剩余18条评论

45

有一个技巧可以让匿名类更新外部作用域中的数据。

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]);

// ...

2
谢谢你的回答。我知道这些,而且我的解决方案比这个更好。我的问题是“为什么只有最终结果”? - user467871
6
那么答案就是这就是它们的实现方式 :) - Ivan Dubrov
1
谢谢。我之前已经自己使用了上面的技巧,但不确定是否是个好主意。如果Java不允许这样做,那么肯定有一个很好的理由。你的回答澄清了我的List.forEach代码是安全的。 - RuntimeException
阅读https://dev59.com/kWnWa4cB1Zd3GeqP0nPN,了解“为什么只有final”的原理讨论。 - lcn
有几种解决方法。我的方法是:final int resf = res; 最初我使用了数组方法,但我发现它的语法太过繁琐。AtomicReference可能会稍微慢一些(分配一个对象)。 - zakmck

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

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

目前我还没有在jls或jvms中找到原因或解释,但我们知道编译器会为每个内部类创建一个单独的类文件,并确保在这个类文件上声明的方法(在字节码层面)至少可以访问局部变量的值。
Jon提供了完整的答案 - 我保留这个链接是因为某些人可能对JLS规则感兴趣)

11

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

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的粗糙、有限版本的闭包),该变量仍保留着创建实例时的值。

因为局部内部类既不是类的成员也不是包的成员,所以它没有声明访问级别。(请注意,它自己的成员具有像普通类中一样的访问级别。)


我提到了“不将其保留为私有成员”。 - user467871
先生,您能否简要说明一下“即使变量已经超出作用域”的意思? - chandu_reddim

9
为了理解这个限制的原因,请考虑以下程序:

要理解这个限制的原因,请考虑以下程序:

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();
    }
}
< p > initialize方法返回后,interfaceInstance仍然存在于内存中,但参数val则不存在。JVM无法访问超出其作用域的局部变量,因此Java通过将val的值复制到同名的隐式字段中来使后续调用printInteger方法生效。interfaceInstance被称为捕获了局部参数的值。如果该参数不是最终的(或有效的最终的),它的值可能会发生更改,与捕获的值不同步,从而可能导致不直观的行为。


6

在Java中,变量可以作为类级别字段使用final关键字,不仅限于参数,例如:

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的意思是:该值不可改变且为最终值。
由于它是final的,Java可以安全地将其复制到本地匿名类中。你并没有获取到int的某个引用(特别是因为在Java中你不能有对像int这样的基本类型的引用,只能有对对象的引用)。
它只是将a的值复制到你的匿名类中的一个隐式整数a中。

3
我将“类级变量”与 static 关键字联系起来。如果你使用“实例变量”这个词可能更加清晰明了。 - eljenso
1
我使用类级别,因为这种技术适用于实例变量和静态变量。 - Zach L
我们已经知道final是可访问的,但我们想知道为什么?您能否在为什么方面添加更多解释? - Saurabh Oza

6

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


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

3
当在方法体内定义匿名内部类时,该方法作用域中声明为 final 的变量都可以从内部类中访问。对于标量值,一旦被赋值,final 变量的值就不会改变。对于对象值,引用不能改变。这使得 Java 编译器能够在运行时“捕获”变量的值,并将其作为内部类中的字段存储一个副本。一旦外部方法终止并且其堆栈帧已经被删除,原始变量就消失了,但是内部类的私有副本仍然存在于该类自己的内存中。
(来源:http://en.wikipedia.org/wiki/Final_%28Java%29

2

匿名内部类中的方法可以在生成它的线程终止后被调用。在您的示例中,内部类将在事件分派线程上调用,而不是在创建它的线程上调用。因此,变量的范围将不同。为了保护这种变量赋值作用域问题,您必须将它们声明为final。


1
private void f(Button b, final int a[]) {

    b.addClickHandler(new ClickHandler() {

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

        }
    });
}

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