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

时间:2011-07-30 05:30:17

标签: java java-bytecode-asm

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

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

  1. 一个简单的POJO,其中包含private int xprivate final int y
  2. MakeFieldsFinalClassAdapter,它使每个字段都访问final,除非它已经是,
  3. 和AddSetYMethodVisitor,它使POJO的setX()方法也将y设置为它设置为x的相同值。
  4. 换句话说,我们从一个带有一个final(x)和一个非final(y)字段的类开始。我们让x最终。除了设置x之外,我们使setX()设置为y。我们跑。 x和y都设置为没有错误。 code is on githubexcerpt in Stephen's answer。您可以使用以下方法克隆它:

    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在运行时愉快地修改了最终字段。

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

    我相信我已经最终证明 JDK7 JVM违反了规范。 (参见JD-GUI。)如前所述,使用ASM修改字节码后,我将其写回到类文件中。使用优秀的{{3}},此类文件将反编译为以下代码:

    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?

3 个答案:

答案 0 :(得分:20)

  

运行时“最终”是最终决定吗?

不是你的意思。

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

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

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

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


<强>更新

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

  

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

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

答案 1 :(得分:5)

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

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

答案 2 :(得分:1)

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