“最终”在运行时是最终的吗?

2022-09-01 21:07:20

我一直在玩ASM,我相信我成功地将最终修饰符添加到类的实例字段;但是,然后我继续实例化所述类并对其调用一个 setter,该 setter 成功更改了现在最终字段的值。我是否在字节码更改时出错,或者最终仅由 Java 编译器强制执行?

更新:(7月31日)这里有一些代码给你。主要部件有

  1. 一个简单的 POJO 与 和 ,private int xprivate final int y
  2. MakeFieldsFinalClassAdapter,它使它访问的每个领域都成为最终的,除非它已经是,
  3. 和 AddSetYMethodVisitor,这会导致 POJO 的 setX() 方法也将 y 设置为与它设置 x 相同的值。

换句话说,我们从一个具有一个最后(x)和一个非最终(y)字段的类开始。我们使 x 成为最终结果。除了设置 x 之外,我们还将 setX() 设置为 y。我们运行。x 和 y 都设置无错误。代码在github上。您可以使用以下命令克隆它:

git clone git://github.com/zzantozz/testbed.git tmp
cd tmp/asm-playground

需要注意的两点:我首先问这个问题的原因是:我最终创建的字段和已经最终的字段都可以使用我认为是正常的字节码指令进行设置。

另一个更新:(8 月 1 日)使用 1.6.0_26-b03 和 1.7.0-b147 进行测试,结果相同。也就是说,JVM 在运行时愉快地修改最终字段。

Final(?)更新:(19 Sep)我从这篇文章中删除了完整的源代码,因为它相当冗长,但它仍然可以在github上使用(见上文)。

我相信我已经最终证明了JDK7 JVM违反了规范。(参见斯蒂芬答案中的摘录。如前所述使用 ASM 修改字节码后,我将其写回类文件。使用出色的 JD-GUI,此类文件反编译为以下代码:

package rds.asm;

import java.io.PrintStream;

public class TestPojo
{
  private final int x;
  private final int y;

  public TestPojo(int x)
  {
    this.x = x;
    this.y = 1;
  }

  public int getX() {
    return this.x;
  }

  public void setX(int x) {
    System.out.println("Inside setX()");
    this.x = x; this.y = x;
  }

  public String toString()
  {
    return "TestPojo{x=" +
      this.x +
      ", y=" + this.y +
      '}';
  }

  public static void main(String[] args) {
    TestPojo pojo = new TestPojo(10);
    System.out.println(pojo);
    pojo.setX(42);
    System.out.println(pojo);
  }
}

简单地看一下应该会告诉你,由于重新分配了最终字段,类永远不会编译,但是在普通的普通JDK 6或7中运行该类看起来像这样:

$ java rds.asm.TestPojo
TestPojo{x=10, y=1}
Inside setX()
TestPojo{x=42, y=42}
  1. 在我报告这方面的错误之前,是否有其他人有意见?
  2. 谁能确认这应该是JDK 6中的错误还是仅在7中?

答案 1

“最终”在运行时是最终的吗?

不是你所指的。

AFAIK,修饰符的语义仅由字节码编译器强制执行。final

没有用于初始化字段的特殊字节码,字节码验证器(显然)也不会检查“非法”分配。final

但是,JIT 编译器可能会将修饰符视为不需要重新提取的提示。因此,如果您的字节码修改了标记为“为”的变量,则可能导致不可预知的行为。(如果您使用反射来修改变量,也会发生同样的事情。规格清楚地说了...)finalfinalfinal

当然,您可以使用反射来修改字段。final


更新

我看了一下Java 7 JVM规范,它部分与我上面所说的相矛盾。具体来说,PutField操作码的描述是:

“链接异常...否则,如果该字段是 final,则必须在当前类中声明它,并且指令必须在当前类的实例初始化方法 (<init>) 中进行。否则,将抛出非法访问错误

因此,虽然(理论上)您可以在对象的构造函数中多次分配给字段,但字节码验证程序应该防止任何加载包含分配给 .哪。。。当您想到Java安全沙箱时...是一件好事。finalfinal


答案 2

如果该字段是最终字段,则在分配到该字段时仍可能出现这种情况。例如,在构造函数中。如本文所述,编译器强制执行此逻辑。JVM本身不会强制执行此类规则,因为性能价格太高,字节码验证器可能无法轻松确定字段是否只分配一次。

因此,通过ASM制作字段可能没有多大意义。final


推荐