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


354
  1. a在这里只能是最终的。为什么?如何aonClick()不保留为私有成员的情况下重新分配方法?

    private void f(Button b, final int a){
        b.addClickHandler(new ClickHandler() {
    
            @Override
            public void onClick(ClickEvent event) {
                int b = a*5;
    
            }
        });
    }
  2. 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 2011年

4
您想达到什么目的?单击处理程序可以在“ f”完成时执行。
伊万·杜布罗夫

@Lambert如果要在onClick方法中使用a,则必须是最终的@Ivan f()方法的行为类似于onClick()方法如何在单击时返回int

2
这就是我的意思-它不支持完全封闭,因为它不允许访问非最终变量。
user541686 2011年

4
注意:从Java 8开始,您的变量只需要有效地定值
Peter Lawrey

Answers:


489

如注释中所述,其中一些在Java 8中变得无关紧要,在Java 8中final可以隐式使用。但是,只能在匿名内部类或lambda表达式中使用有效的最终变量。


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

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

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

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

如果您想对Java和C#闭包之间的更详细的比较感兴趣,请参阅我的文章。我想在这个答案中专注于Java方面:)


4
@Ivan:基本上像C#。但是,如果要使用与C#相同的功能,则可以将来自不同作用域的变量“实例化”不同的次数,因此它具有相当的复杂性。
乔恩·斯基特


11
对于Java 7来说都是如此,请记住,随着Java 8的引入,闭包现在确实可以从其内部类访问类的非最终字段。
Mathias Bader 2014年

22
@MathiasBader:真的吗?我认为它实际上仍然是相同的机制,编译器现在足够聪明以进行推断final(但是仍然需要有效地最终确定)。
Thilo 2016年

3
@Mathias Bader:您始终可以访问非final 字段,不要将它们与局部变量相混淆,局部变量必须是final并且仍然必须是有效final,因此Java 8不会更改语义。
Holger

41

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

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
感谢您的回答。我知道所有这些,我的解决方案比这更好。我的问题是“为什么只有决赛”?

6
答案是,这就是它们的实现方式:)
Ivan Dubrov 2011年

1
谢谢。我自己使用了上述技巧。我不确定这是否是个好主意。如果Java不允许,则可能有充分的理由。您的回答说明我的List.forEach代码是安全的。
2015年

阅读stackoverflow.com/q/12830611/2073130,以更好地讨论“为什么只有最终语言”的基本原理。
lcn

有几种解决方法。我的是:final int resf = res; 最初,我使用数组方法,但是发现它的语法过于繁琐。AtomicReference可能要慢一些(分配一个对象)。
zakmck

17

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

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

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

乔恩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类,并且可以访问其包含方法的最终局部变量。当实例使用其包含方法的最终局部变量时,即使变量超出范围,变量也会保留其在实例创建时所保存的值(这实际上是Java的闭包的原始版本)。

因为本地内部类既不是类也不是包的成员,所以不会使用访问级别声明它。(但是请注意,它自己的成员具有与普通班级相同的访问级别。)


我提到“不保留为私人成员”

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意味着:该值是不变的且是最终的。

而且由于它是最终版本,因此Java可以安全地复制到本地匿名类中。您没有获得对int的某种引用(尤其是因为您无法像Java中那样引用诸如int之类的基元,而仅具有对Objects的引用)。

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


3
我将“类级变量”与关联 static。也许更清楚的是,如果您使用“实例变量”代替。
eljenso 2011年

1
好吧,我使用了类级别的,因为该技术可以同时使用实例变量和静态变量。
Zach L

我们已经知道final可以访问,但是我们想知道为什么?能否请您解释一下为什么要添加更多内容?
萨拉巴·奥扎

6

之所以只将访问限制在局部最终变量上,是因为如果所有局部变量都可以访问,则首先需要将它们复制到一个单独的部分,内部类可以访问这些部分并维护它们的多个副本。可变的局部变量可能会导致数据不一致。最终变量是不可变的,因此对其进行任何数量的复制不会对数据的一致性产生任何影响。


这不是通过支持该功能的C#语言实现的。实际上,编译器将变量从局部变量更改为实例变量,或者为这些变量创建了额外的数据结构,这些数据结构的寿命可能超出外部类的范围。但是,没有“局部变量的多个副本”
Mike76 '16

Mike76我没有看过C#的实现,但是Scala做了您提到的第二件事,我想:如果将an Int重新分配给闭包内部,则将该变量更改为IntRef(本质上是可变Integer包装器)的实例。然后,将相应地重写每个变量访问。
Adowrath

3

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

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();
    }
}

interfaceInstance保留在内存后初始化方法的返回值,但参数VAL没有。JVM无法访问其范围之外的局部变量,因此Java 通过将val的值复制到interfaceInstance内具有相同名称的隐式字段来使对printInteger的后续调用起作用。该interfaceInstance据说已抓获本地参数的值。如果该参数不是最终参数(或有效最终参数),则其值可能会更改,从而与捕获的值不同步,从而可能导致不直观的行为。


2

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


2

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

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


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

    b.addClickHandler(new ClickHandler() {

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

        }
    });
}

0

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

考虑用例,其中您的lambda而不是被应用,而是存储在某个位置并稍后运行。

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


0

试试这个代码,

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

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正在询问为什么需要某些东西的原因。因此,您应该指出您的代码如何解决它
NitinSingh

0

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

因此,为了不将Java Developer与那些来自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决定强制将其定为最终版本的原因,因此数据从始至终都是“一致的”。


0

内部类中的Java最终变量

内部类只能使用

  1. 来自外部阶级的参考
  2. 超出范围的最终局部变量是引用类型(例如Object...)
  3. 值(原始)(例如int...)类型可以由最终引用类型包装IntelliJ IDEA可以帮助您将其隐藏到一个元素数组中

当编译器生成non static nestedinner class[About]时,将创建一个新类,<OuterClass>$<InnerClass>.class并将绑定的参数传递到构造函数[栈中的本地变量]中。它类似于关闭

最终变量是不能重新分配的变量。最终参考变量仍然可以通过修改状态来更改

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

//Not possible 
private void foo() {

    MyClass myClass = new MyClass(); //address 1
    int a = 5;

    Button button = new Button();

    //just as an example
    button.addClickHandler(new ClickHandler() {


        @Override
        public void onClick(ClickEvent event) {

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

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

    myClass = new MyClass(); //address 2
    int a = 10;
}

-2

也许这个把戏给你一个主意

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);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.