24

私はASMをいじってきましたが、クラスのインスタンス フィールドに final 修飾子を追加することに成功したと思います。ただし、その後、上記のクラスをインスタンス化し、セッターを呼び出して、現在の最終フィールドの値を正常に変更しました。バイトコードの変更に何か問題がありますか、それとも最終的には Java コンパイラによってのみ強制されますか?

更新: (7 月 31 日) ここにいくつかのコードがあります。主なパーツは

  1. private int xprivate final int yを含む単純な POJO
  2. MakeFieldsFinalClassAdapter は、アクセスするすべてのフィールドを最終的なものにします。
  3. AddSetYMethodVisitor は、POJO の setX() メソッドによって、x に設定した値と同じ値に y も設定します。

つまり、1 つの最終 (x) フィールドと 1 つの非最終 (y) フィールドを持つクラスから始めます。x を final にします。x の設定に加えて、setX() で y も設定します。私たちは走る。x と y の両方がエラーなしで設定されます。コードは github にあります。次の方法で複製できます。

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

2 つの注意事項:そもそもこの質問をした理由: final にしたフィールドと既に final になっているフィールドの両方に、通常のバイトコード命令であると思われるものを設定できます。

別の更新: (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);
  }
}

これを少し見てみると、 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がスローされます。」

したがって、(理論的には)finalオブジェクトのコンストラクターでフィールドに複数回割り当てることはできますが、バイトコードベリファイアは、に割り当てるバイトコードを含むメソッドをロードしようとする試みを防ぐ必要がありfinalます。これは...Javaセキュリティサンドボックスについて考えるとき...良いことです。

于 2011-07-30T08:05:09.390 に答える
5

フィールドが final の場合、割り当てられたときにまだ状況がある可能性があります。たとえば、コンストラクターで。このロジックは、この記事で説明されているように、コンパイラによって適用されます。JVM 自体は、パフォーマンス プライスが高すぎるため、そのようなルールを強制しません。また、フィールドが 1 回しか割り当てられていないかどうかをバイト コード ベリファイアが簡単に判断できない可能性があります。

したがって、finalASM を介してフィールドを作成することは、おそらくあまり意味がありません。

于 2011-07-30T06:00:16.880 に答える
1

リフレクションを使用して、実行時に final フィールドを上書きできます。Gson は、JSON を Java オブジェクトにバインドしている間、常にこれを行います。

于 2014-08-29T17:22:53.337 に答える