【问题标题】:Is "final" final at runtime?“最终”在运行时是最终的吗?
【发布时间】:2011-07-30 05:30:16
【问题描述】:

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

更新:(7 月 31 日)这里有一些代码供您参考。主要部分是

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

换句话说,我们从一个具有一个 final (x) 和一个 non-final (y) 字段的类开始。我们使 x 最终。除了设置 x 之外,我们还让 setX() 设置 y。我们跑。 x 和 y 都设置没有错误。 code is on 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 违反了规范。 (参见excerpt in 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 中的错误还是 JDK 7 中的错误?

【问题讨论】:

  • 您介意分享相关代码吗?

标签: java java-bytecode-asm


【解决方案1】:

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

不是你的意思。

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

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

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

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


更新

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

"Linking Exceptions ...否则,如果该字段是final,则必须在当前类中声明,并且该指令必须出现在实例初始化方法中 (<init>) 当前班级。否则,将抛出 IllegalAccessError。".

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

【讨论】:

  • 我还没有在JDK7上测试过,只有6个。今晚应该有时间在7上试试。
  • 用 1.7.0-b147 测试并得到相同的结果。
  • @Ryan Stewart - 您应该测试的是构造函数可以更改最终实例变量,但尝试更改最终实例变量的常规方法会触发加载时错误。 (我不认为 Java 6 和 Java 7 会有所不同。)
  • 我就是这么做的。我从一个 POJO 开始,它有一个 final、一个 non-final 字段和非 final 字段的 getter/setter。使用 ASM,我生成了一个新类,它具有两个字段 final,并且在现有的 setter 中,两个字段都发生了变异。它加载(使用 ClassLoader.defineClass())并运行没有任何问题。
  • @Ryan Stewart - 我能说清楚吗?您是说您手动生成的代码正在加载和运行违反我上面引用的 JVM 规范部分?那是对的吗?如果是这样,那是一个 JVM 错误……应该报告给 Oracle!
【解决方案2】:

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

所以通过 ASM 创建字段 final 可能没有多大意义。

【讨论】:

    【解决方案3】:

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

    【讨论】:

    • Gson 库在内部完成
    猜你喜欢
    • 2011-02-15
    • 2016-05-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-14
    • 2011-04-09
    • 1970-01-01
    相关资源
    最近更新 更多