运行时"最终"是最终的吗?

Rya*_*art 24 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的相同值.

换句话说,我们从一个具有一个final(x)和一个非final(y)字段的类开始.我们让x最终.除了设置x之外,我们使setX()设置为y.我们跑.x和y都设置为没有错误.该代码在GitHub上.你可以用以下方法克隆它:

git clone git://github.com/zzantozz/testbed.git tmp
cd tmp/asm-playground
Run Code Online (Sandbox Code Playgroud)

需要注意的两件事:我首先提出这个问题的原因是:我做了最后一个字段和一个已经是最终字段的字段都可以设置为我认为是正常的字节码指令.

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

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

我相信我已经最终证明JDK7 JVM违反了规范.(请参阅Stephen的回答摘录.)如前所述,在使用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);
  }
}
Run Code Online (Sandbox Code Playgroud)

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

$ java rds.asm.TestPojo
TestPojo{x=10, y=1}
Inside setX()
TestPojo{x=42, y=42}
Run Code Online (Sandbox Code Playgroud)
  1. 在我报告错误之前,有没有其他人有输入?
  2. 任何人都可以确认这应该是JDK 6中的错误还是只有7?

Ste*_*n C 20

运行时"最终"是最终的吗?

不是你的意思.

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

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

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

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


UPDATE

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

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

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


Ale*_*man 5

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

所以final通过ASM 创建这个领域可能没什么意义.