使用在匿名Runnable中的final int参数

3

我想知道我是否使用了正确的方法在不同的线程中调用某些东西。我正在Android上进行操作,但认为这是通用的Java问题。

我有一个带有一些参数的方法。假设它们是int。

class Main1 {
  public static void mainOnlyWork(int x, int y) {
     // not important here.
  }
}

class Test {
  Handler mHandler;

  // called from main thread, stored as reference.
  public Test() {
    mHandler = new Handler();
  }

  public static void callInMainThread(final int x, final int y) {
    mHandler.post(new Runnable() {
      public void run() {
        Main1.mainOnlyWork(x, y);
      }
    });
  }

现在我的问题是,是否可以安全地使用final int来创建匿名runnable而不使用任何类成员?如果我省略了x和y参数的final关键字,Eclipse会发出警告。在这种情况下,似乎只能使用常数。如果我传递的不是常数,可以吗?Java会通过将其传递给此函数来“使”它成为常数吗?
但是我想从本机使用JNI调用Test.callInMainThread。在我看来,Java无法判断这些数字是否为常量。我能相信Java会进行一些魔法吗?它总是这样工作吗?
我认为也许我必须创建代理类,像:
private abstract RunnableXY implements Runnable {
  public RunnableXY(int x, int y) {
    this.x = x;
    this.y = y;
  }

  public int x;
  public int y;

  public abstract void run();
}

并且调用方法将会使用:

  public static void callInMainThread(final int x, final int y) {
    mHandler.post(new RunnableXY(x,y) {
      public void run() {
        Main1.mainOnlyWork(this.x, this.y);
      }
    });
  }

这样,我可以保护值免受垃圾回收,直到可运行对象被使用和丢弃。我需要创建包装器吗?还是在方法参数中标记final x是安全的?当我尝试时,final关键字可以正常工作。但是在线程中,我不认为它现在能够工作就意味着它将永远工作。final总是有效吗?如果有效,它是如何实现的?如果参数不是基本类型而是对象,会有区别吗?

更新: 我已经了解了Java中final的含义。这不是问题的重点。问题的重点是Runnable的创建中使用的变量的范围在哪里。它们是局部变量,这意味着在函数结束后它们的值不能被引用。那么当Runnable对象传递到其他线程并等待执行时,这些值存储在哪里?


我认为我已经找到了答案。这些参数必须是final的,因为它们在生成匿名Runnable的run()方法时被使用。如果我运行callInMainThread(x=3, y=5),public void run()将会生成Main1.mainOnlyWork(3, 5)的主体,不引用任何变量,无论是局部还是全局的。这就是我所寻求的。它存储在在那一点上创建的Runnable.run()代码中。并且当在线程之间传递时,代码不会改变。 - Pihhan
3个回答

3

在线程中会复制参数和本地变量(存储在方法调用栈上),这是因为方法调用结束后,线程仍然存在。

并且要求变量为final,以禁止覆盖方法中的原始变量,否则会在线程中产生不同版本,这简直是误导性的。所以问题在于让同名的两个版本表示相同的含义。

需要仔细设计语言。

(简化了一些内容,没有命名对称反转的情况,其中也适用同样的规则。)


2

从基础开始:Java在传递参数时会进行复制。方法接收到的x和y不是传入的原始变量,而是原始值的副本。当您声明一个如此的方法时,传入的值不必是常量,但该方法接收到的副本是1

callInMainThread(final int x, final int y) { ..... }

第二点:不需要创建包装器。当您访问局部变量时,Java编译器会自动为其生成字段,就像您手动创建的包装器一样。这对您来说是透明的。
您不能省略final的一个原因是,Java没有实现任何机制来在方法的局部变量和匿名类中生成的字段之间传递变量值的更改。此外,匿名类可能比方法调用的生命周期更长。如果匿名类在方法返回后读取或写入变量会发生什么?如果变量不是final,则无法再读取其值或写入其中。
注1:实际上,final变量不是常量。您可以将不同的值分配给它们,但只能分配一次。 final方法参数的值在调用方法时分配,因此它们在方法的持续时间内基本上是恒定的。

啊,谢谢!我一开始并没有理解第三段具体是什么意思,直到我用不同的措辞自己想明白了。 - Pihhan

1

原始类型始终按值传递。即使在方法内修改它们,它们的原始值(方法外部)也永远不会改变。因此,是安全的,因为这些值仅局限于该方法(无论它们是否为final)。

关于参数中的final关键字,它实际上只阻止您重新分配该值,没有其他目的。这是为了纯粹的代码安全性。在Java中,参数始终按值传递(对象引用也按值传递),因此无论如何,任何重新分配都将局限于该方法。一旦方法完成,您在方法中进行的所有重新分配都将消失。例如,以下两种方式没有区别:

public void test(String a) {
    a = "Hello";
}

"and"。
public void test(final String a) {
    a = "Hello";
}

除了编译器会在第二种情况下引发错误之外,两种情况都一样。在第一种情况下,当test()方法完成时,a将恢复到原始值(它不会是"Hello")。因此,参数中的 effectively final 使参数“常量”(请注意,对于对象:您仍然可以修改对象状态,但不能修改对象的引用)。
在匿名类中,需要使用 final 关键字,因为您正在引用另一个类范围内的变量(在您的情况下,您的匿名 Runnable 实例正在引用 Main1 实例变量)。因此,如果在另一个线程中运行并且它们不是 final,则可以在方法的持续时间内覆盖原始引用。这将使每个线程引用具有相同变量名称的不同对象,这很令人困惑,因此在语言设计中被阻止。

您不需要创建任何包装器或额外的引用。参数已经在方法的持续时间内被引用,并且不会被垃圾回收。


它们确实是函数本地的事实让我担心。如果我用不同的参数连续三次调用它,那么在主线程开始执行第一个函数之前,方法参数已经改变了3次。这些本地值存储在哪里?如果在本地线程堆栈中,那么可能会被覆盖。主线程不应该访问工作线程堆栈,对吗?这些整数是否在堆内存中,而堆内存是线程之间共享的? - Pihhan
本地变量存储在堆栈中,因为它们是临时存储。每个线程都有自己的堆栈,它不是共享的。这是线程的本地上下文。除了当前线程上下文,它们不能被覆盖在本地线程堆栈中,无论它们从哪里调用或多快 - 实际上我无法理解你确切的意思。此外,再次强调,这些值是复制的,因此无论它们在本地范围内如何更改,您都在专门更改该本地副本而不是另一个副本和原始值。 - m0skit0
那么我的问题是它们如何被复制到新的Runnable() {}中?它们是否作为该匿名对象的字段进行创建?我想知道这些变量是如何发送到另一个线程的。我理解什么是局部变量。但是当调用该方法时,这些变量并未被使用。它们被传递给将来要调用的方法。它们在之间的时间是在哪里? - Pihhan
当您调用该方法时,也会创建Runnable。因为匿名类在该范围内,所以Runnable使用的值将是传递给该方法的值。它们如何复制到匿名接口实际上取决于Java对匿名类/接口的实现。在我看来,这是无关紧要的:作为字段或其他方式进行复制。如果您真的想了解详细信息,可以查看JRE源代码。 - m0skit0
这就是我所询问的。Java/Dalvik是否确保变量始终完好无损,它是如何做到的?如果我确信虚拟机确保它们永远不会被覆盖或无效,则可能与此无关。我不够熟练,无法理解JRE源代码并确定其是否安全。我希望有更聪明的人知道答案。 - Pihhan
我仍然不明白你所说的“确保变量始终完整”的意思。final变量在编译时会被检查,因此无法更改。再次强调:参数是副本,本地上下文可以以任何方式修改它,但不会影响其他线程的相同变量。“它是如何做到的”请查看源代码。如果您没有理解JRE源代码的知识,则无法理解它是如何实现的。无论如何,“如何”实际上并不重要。此外,这可能会根据JRE开发人员的不同而以不同的方式实现。您可以假设JRE正在正确执行它。 - m0skit0

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