【问题标题】:Access variables from outer methods从外部方法访问变量
【发布时间】:2020-08-06 15:32:32
【问题描述】:

我正在学习 Java 中的内部类,但我遇到了与外部方法中的变量引用有关的问题。例如,我有一个源代码可以统计排序过程中调用compareTo()方法的次数:

        int counter = 0;
        Date[] dates = new Date[100];
        for(int i = 0; i < dates.length; i++)
        {
            dates[i] = new Date()
            {
                public int compareTo(Date other)
                {
                    counter++;
                    return super.compareTo(other);
                }
            };
        }
        Arrays.sort(dates);
        System.out.println(counter + " comparisons");

在执行源码时,可以看到counter++的使用存在错误。为了解决这个问题,有人告诉我应该这样改:

        int[] counter = new int[1];
        Date[] dates = new Date[100];
        for(int i = 0; i < dates.length; i++)
        {
            dates[i] = new Date()
            {
                public int compareTo(Date other)
                {
                    counter[0]++;
                    return super.compareTo(other);
                }
            };
        }
        Arrays.sort(dates);
        System.out.println(counter[0] + " comparisons");

我很困惑这两个代码有什么区别,这个错误的原因和解决方法是什么?

【问题讨论】:

  • 您只能在匿名类和 lambda 表达式中使用最终/有效最终变量。使用int[] 是一种解决方法(这可能适合您的情况,也可能不适合您的情况),因为变量counter 被编译器识别为final,但其中的元素仍然可以更改。相关:Why are only final variables accessible in anonymous class?。另见atomic variables
  • @user 我很抱歉,但是如何将 int counter 转换为 int[] counter 将计数器更改为实际上是最终的
  • 转换为数组并不能使其有效地最终化。在第一个示例中,您使用counter++ 更改了counter 的值,因此它不是最终的。但是,在第二个示例中,您并没有更改变量 counter 本身,而是更改了其中的元素,因此 counter 实际上是最终的。如果你不确定一个变量是否是有效的最终变量,请将其标记为final 并查看编译器是否抱怨

标签: java class oop inner-classes


【解决方案1】:

您正在创建一个可以“旅行”的代码 sn-p。 {} 中属于您的 new Date() 声明的代码不在您编写它的地方运行;它附加到您制作的这个日期对象上,并与它一起使用。此日期对象可以旅行:它可以存储在字段中。也许它在 18 天后运行,在一个完全不同的线程中。 VM 不知道,所以它需要为此做好准备。

假设它确实如此:你的“计数器”变量会发生什么?

通常,局部变量存储在“堆栈”上,并在方法退出时被销毁。但在这种情况下,我们将销毁您的旅行代码仍然可以访问的变量,那么这意味着,从现在起 18 天后,当您的日期 compareTo 代码被调用时?

假设虚拟机默默地“升级”变量;它不是像往常一样在堆栈上声明它,而是在堆上声明它,以便变量可以在方法退出时继续存在。

好的。如果在另一个线程中调用 compareTo 怎么办?现在是否可以将局部变量标记为“易失性”?是否可以声明,在 java 中,即使是局部变量也可能显示竞争条件?

这是一个判断性的电话;由语言设计者决定。

Java 的语言设计者决定反对静默升级到堆并反对允许本地人可能受到多线程访问。

因此,您在可以“旅行”的任何代码块中访问的任何局部变量*必须将 [A] 声明为 final 或 [B] 表现得好像它本来可以那样,在这种情况下,java 会默默地做到这一点最后给你。

变化在于counter,变量本身,在第二个sn-p中没有改变:它是一个数组的引用,并且这个引用永远不会改变。实际上,您已经自己添加了间接级别和堆访问:堆上存在数组。

对于它的价值,我发现 AtomicX 的使用更具可读性。因此,如果您需要一个可在旅行代码中修改的 int,请不要这样做 new int[1];做new AtomicInteger。如果您需要可修改的字符串,请使用new AtomicReference&lt;String&gt;(),而不是new String[1]

NB:是的,在 this 特定代码中,即使是排序操作,在此方法中也仅使用 counter 变量,并且一旦此方法结束,counter var 可以消失,但编译器没有做那种非常深入的分析来解决这个问题,它使用了更简单的规则:想要在“旅行”代码中从外部 scipe 访问本地变量?不允许 - 除非它(有效)是最终的。

*) 旅行代码是方法本地或匿名类定义中的任何内容,以及 lambda 中的任何内容。所以:


void method() {
    class MethodLocalClassDef {
        // anything here is considered 'travelling'
    }

    Object o = new Object() {
        // this is an anonymous class def,
        // and anything in here is 'travelling'
    };

    Runnable r = () -> {
        // this is a lambda, and considered 'travelling'
    };
}

【讨论】:

  • 您的解释非常棒,甚至解释了在本地方法中使用final 变量的原因。但我有一个问题:Java 是一种高级语言,具有“垃圾收集”模式。怎么保证程序终止后counter在Heap上的数据也能保存?
  • +1 on Atomic*,不仅是为了可读性,而且如果你正在编写快速而肮脏的并发代码,只要你适当地使用它们,它们就可以省去很多麻烦和样板代码。跨度>
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多