24

我一直在玩弄ASM,我相信我成功地将 final 修饰符添加到类的实例字段中;但是,然后我继续实例化所述类并在其上调用 setter,这成功地更改了 now-final 字段的值。我的字节码更改是否有问题,或者最终仅由 Java 编译器强制执行?

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

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

换句话说,我们从一个具有一个 final (x) 和一个 non-final (y) 字段的类开始。我们使 x 最终。除了设置 x 之外,我们还让 setX() 设置 y。我们跑。x 和 y 都设置没有错误。代码在github 上。您可以使用以下命令克隆它:

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

有两件事需要注意:我首先提出这个问题的原因是:我设置为 final 的字段和已经设置为 final 的字段都可以使用我认为是正常的字节码指令进行设置。

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

最终(?)更新:(9 月 19 日)我从这篇文章中删除了完整的源代码,因为它相当长,但它仍然可以在 github 上找到(见上文)。

我相信我已经最终证明JDK7 JVM 违反了规范。(参见Stephen's answer 中的摘录。)在使用 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);
  }
}

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

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

3 回答 3

19

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

不是你的意思。

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

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

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

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


更新

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

“链接异常......否则,如果该字段是final,则必须在当前类中声明,并且该指令必须出现在当前类的实例初始化方法(<init>)中。否则,IllegalAccessError抛出an。” .

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

于 2011-07-30T08:05:09.390 回答
5

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

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

于 2011-07-30T06:00:16.880 回答
1

您可以在运行时使用反射覆盖最终字段。Gson 在将 JSON 绑定到 Java 对象时一直这样做。

于 2014-08-29T17:22:53.337 回答