かなり長い間Javaバイトコードを操作して、この問題についていくつかの追加調査を行った後、これが私の発見の要約です。
スーパーコンストラクターまたは補助コンストラクターを呼び出す前にコンストラクターでコードを実行する
Javaプログラミング言語(JPL)では、コンストラクターの最初のステートメントは、スーパーコンストラクターまたは同じクラスの別のコンストラクターの呼び出しでなければなりません。これは、Javaバイトコード(JBC)には当てはまりません。バイトコード内では、コンストラクターの前にコードを実行することは絶対に正当です。
- このコードブロックの後で、互換性のある別のコンストラクターが呼び出されます。
- この呼び出しは条件付きステートメント内ではありません。
- このコンストラクター呼び出しの前に、構築されたインスタンスのフィールドは読み取られず、そのメソッドは呼び出されません。これは次の項目を意味します。
スーパーコンストラクターまたは補助コンストラクターを呼び出す前にインスタンスフィールドを設定する
前述のように、別のコンストラクターを呼び出す前にインスタンスのフィールド値を設定することは完全に合法です。6より前のJavaバージョンでこの「機能」を利用できるようにするレガシーハックも存在します。
class Foo {
public String s;
public Foo() {
System.out.println(s);
}
}
class Bar extends Foo {
public Bar() {
this(s = "Hello World!");
}
private Bar(String helper) {
super();
}
}
この方法では、スーパーコンストラクターが呼び出される前にフィールドを設定できますが、これはできなくなりました。JBCでは、この動作は引き続き実装できます。
スーパーコンストラクター呼び出しを分岐させる
Javaでは、次のようなコンストラクター呼び出しを定義することはできません。
class Foo {
Foo() { }
Foo(Void v) { }
}
class Bar() {
if(System.currentTimeMillis() % 2 == 0) {
super();
} else {
super(null);
}
}
ただし、Java 7u23までは、HotSpot VMのベリファイアはこのチェックに失敗していました。そのため、これが可能でした。これはハックの一種としていくつかのコード生成ツールで使用されましたが、このようなクラスを実装することはもはや合法ではありません。
後者は、このコンパイラバージョンの単なるバグでした。新しいコンパイラバージョンでは、これも可能です。
コンストラクターなしでクラスを定義する
Javaコンパイラーは常に、任意のクラスに対して少なくとも1つのコンストラクターを実装します。Javaバイトコードでは、これは必要ありません。これにより、リフレクションを使用しても構築できないクラスを作成できます。ただし、を使用sun.misc.Unsafe
すると、そのようなインスタンスを作成できます。
シグネチャは同じであるが戻り値の型が異なるメソッドを定義する
JPLでは、メソッドはその名前と生のパラメータタイプによって一意として識別されます。JBCでは、生の戻り値の型がさらに考慮されます。
名前ではなくタイプのみが異なるフィールドを定義する
クラスファイルには、異なるフィールドタイプを宣言する限り、同じ名前の複数のフィールドを含めることができます。JVMは常にフィールドを名前とタイプのタプルとして参照します。
未宣言のチェック済み例外をキャッチせずにスローする
JavaランタイムとJavaバイトコードは、チェックされた例外の概念を認識していません。チェックされた例外がスローされた場合、常にキャッチまたは宣言されていることを確認するのはJavaコンパイラのみです。
ラムダ式の外で動的メソッド呼び出しを使用する
いわゆる動的メソッド呼び出しは、Javaのラムダ式だけでなく、あらゆる目的に使用できます。この機能を使用すると、たとえば、実行時に実行ロジックを切り替えることができます。JBCまで煮詰めた多くの動的プログラミング言語は、この命令を使用することでパフォーマンスを向上させました。Javaバイトコードでは、Java 7でラムダ式をエミュレートすることもできます。JVMはすでに命令を理解しているのに、コンパイラはまだ動的メソッド呼び出しの使用を許可していません。
通常は正当とは見なされない識別子を使用する
メソッド名にスペースと改行を使用することを考えたことはありますか?独自のJBCを作成し、コードレビューのために頑張ってください。識別子の唯一の不正な文字は.
、;
、[
と /
。さらに、名前が付いていない、<init>
またはandを<clinit>
含むことができないメソッド。<
>
final
パラメータまたはthis
参照を再割り当てします
final
パラメータはJBCに存在しないため、再割り当てすることができます。this
参照を含むすべてのパラメーターは、単一のメソッドフレーム内のthis
インデックス0
で参照を再割り当てできるようにするJVM内の単純な配列にのみ格納されます。
final
フィールドを再割り当て
finalフィールドがコンストラクター内で割り当てられている限り、この値を再割り当てすることも、値をまったく割り当てないこともできます。したがって、次の2つのコンストラクターは有効です。
class Foo {
final int bar;
Foo() { } // bar == 0
Foo(Void v) { // bar == 2
bar = 1;
bar = 2;
}
}
以下のためにstatic final
フィールド、でも、クラス初期化子の外のフィールドを再割り当てすることが許可されています。
コンストラクタとクラス初期化子を、それらがメソッドであるかのように扱います。
これはより概念的な機能ですが、JBC内では、コンストラクターは通常のメソッドと同じように扱われます。コンストラクタが別の正当なコンストラクタを呼び出すことを保証するのは、JVMのベリファイアのみです。それ以外は、コンストラクタを呼び出さなければならず<init>
、クラス初期化子が呼び出されるのは単なるJava命名規則です<clinit>
。この違いを除けば、メソッドとコンストラクタの表現は同じです。Holgerがコメントで指摘したように、void
これらのメソッドを呼び出すことはできなくても、引数以外の戻り値の型を持つコンストラクターやクラス初期化子を定義することもできます。
非対称レコード*を作成します。
レコードを作成するとき
record Foo(Object bar) { }
javacは、という名前の単一のフィールド、という名前bar
のアクセサメソッド、bar()
および単一のをとるコンストラクタを持つクラスファイルを生成しますObject
。さらに、のレコード属性bar
が追加されます。レコードを手動で生成することにより、異なるコンストラクターシェイプを作成し、フィールドをスキップして、アクセサーを異なる方法で実装できます。同時に、クラスが実際のレコードを表すとリフレクションAPIに信じ込ませることも可能です。
スーパーメソッドを呼び出す(Java 1.1まで)
ただし、これはJavaバージョン1および1.1でのみ可能です。JBCでは、メソッドは常に明示的なターゲットタイプでディスパッチされます。これは、
class Foo {
void baz() { System.out.println("Foo"); }
}
class Bar extends Foo {
@Override
void baz() { System.out.println("Bar"); }
}
class Qux extends Bar {
@Override
void baz() { System.out.println("Qux"); }
}
ジャンプしながらQux#baz
呼び出すように実装することが可能でしFoo#baz
たBar#baz
。直接のスーパークラスのスーパーメソッド実装とは別のスーパーメソッド実装を呼び出すための明示的な呼び出しを定義することは引き続き可能ですが、これは1.1以降のJavaバージョンではもはや効果がありません。Java 1.1では、この動作はACC_SUPER
、直接のスーパークラスの実装のみを呼び出すのと同じ動作を有効にするフラグを設定することによって制御されていました。
同じクラスで宣言されているメソッドの非仮想呼び出しを定義する
Javaでは、クラスを定義することはできません
class Foo {
void foo() {
bar();
}
void bar() { }
}
class Bar extends Foo {
@Override void bar() {
throw new RuntimeException();
}
}
上記のコードでは、常にのインスタンスでRuntimeException
when foo
が呼び出されますBar
。メソッドを定義して、で定義されている独自のメソッドをFoo::foo
呼び出すことはできません。非プライベートインスタンスメソッドで、呼び出しは常に仮想です。バイトコードを使用すると、1はしかし、使用する呼び出しを定義することができます直接リンクオペコードでメソッド呼び出しをするのバージョンを。このオペコードは通常、スーパーメソッド呼び出しを実装するために使用されますが、オペコードを再利用して、説明されている動作を実装できます。 bar
Foo
bar
INVOKESPECIAL
bar
Foo::foo
Foo
細粒度の型注釈
Javaでは、アノテーションは、アノテーションが@Target
宣言する内容に従って適用されます。バイトコード操作を使用すると、このコントロールとは無関係に注釈を定義できます。また、たとえば、@Target
注釈が両方の要素に適用される場合でも、パラメータに注釈を付けずにパラメータタイプに注釈を付けることが可能です。
タイプまたはそのメンバーの属性を定義する
Java言語内では、フィールド、メソッド、またはクラスのアノテーションのみを定義できます。JBCでは、基本的にあらゆる情報をJavaクラスに埋め込むことができます。ただし、この情報を利用するために、Javaクラスのロードメカニズムに依存することはできなくなりましたが、メタ情報を自分で抽出する必要があります。
オーバーフローと暗黙的に割り当てbyte
、short
、char
およびboolean
値
後者のプリミティブ型は、JBCでは通常知られていませんが、配列型またはフィールドおよびメソッド記述子に対してのみ定義されています。バイトコード命令内では、すべての名前付き型は32ビットのスペースを使用し、として表すことができますint
。公式には、唯一のint
、float
、long
およびdouble
タイプは、バイトコードJVMの検証のルールによって、すべての必要明示的な変換の中に存在します。
モニターを解放しない
synchronized
ブロックは、実際には二つの文、取得してモニタを解放するために1:1で構成されています。JBCでは、リリースせずに取得できます。
注:HotSpotの最近の実装では、これは代わりにIllegalMonitorStateException
メソッドの最後の、またはメソッド自体が例外自体によって終了した場合の暗黙のリリースにつながります。
return
型初期化子に複数のステートメントを追加する
Javaでは、次のような自明な型初期化子でさえ
class Foo {
static {
return;
}
}
違法です。バイトコードでは、型初期化子は他のメソッドと同じように扱われます。つまり、returnステートメントはどこにでも定義できます。
既約ループを作成する
Javaコンパイラーは、ループをJavaバイトコードのgotoステートメントに変換します。このようなステートメントを使用して、Javaコンパイラーが行うことのできない既約ループを作成できます。
再帰的なcatchブロックを定義する
Javaバイトコードでは、ブロックを定義できます。
try {
throw new Exception();
} catch (Exception e) {
<goto on exception>
throw Exception();
}
synchronized
Javaでブロックを使用すると、同様のステートメントが暗黙的に作成され、モニターの解放中に例外が発生すると、このモニターを解放する命令に戻ります。通常、そのような命令では例外は発生しませんが、発生した場合(非推奨などThreadDeath
)、モニターは解放されます。
デフォルトのメソッドを呼び出す
Javaコンパイラーは、デフォルトのメソッドの呼び出しを許可するために、いくつかの条件を満たす必要があります。
- メソッドは最も具体的なものでなければなりません(スーパータイプを含む任意のタイプによって実装されるサブインターフェースによってオーバーライドされてはいけません)。
- デフォルトメソッドのインターフェースタイプは、デフォルトメソッドを呼び出すクラスによって直接実装される必要があります。ただし、
B
インターフェースA
がインターフェースを拡張するものの、のメソッドをオーバーライドしない場合A
でも、メソッドを呼び出すことができます。
Javaバイトコードの場合、2番目の条件のみがカウントされます。ただし、最初のものは無関係です。
でないインスタンスでスーパーメソッドを呼び出す this
Javaコンパイラでは、のインスタンスでのみスーパー(またはインターフェースのデフォルト)メソッドを呼び出すことができますthis
。ただし、バイトコードでは、次のような同じタイプのインスタンスでスーパーメソッドを呼び出すこともできます。
class Foo {
void m(Foo f) {
f.super.toString(); // calls Object::toString
}
public String toString() {
return "foo";
}
}
合成メンバーにアクセスする
Javaバイトコードでは、合成メンバーに直接アクセスできます。たとえば、次の例で、別のBar
インスタンスの外部インスタンスにアクセスする方法を考えます。
class Foo {
class Bar {
void bar(Bar bar) {
Foo foo = bar.Foo.this;
}
}
}
これは一般的に、あらゆる合成フィールド、クラス、またはメソッドに当てはまります。
非同期のジェネリック型情報を定義する
Javaランタイムはジェネリック型を処理しませんが(Javaコンパイラーが型消去を適用した後)、この情報はコンパイルされたクラスにメタ情報として添付され、リフレクションAPIを介してアクセス可能になります。
ベリファイアは、これらのメタデータでString
エンコードされた値の整合性をチェックしません。したがって、消去と一致しないジェネリック型に関する情報を定義することが可能です。結果として、以下の主張が当てはまる可能性があります。
Method method = ...
assertTrue(method.getParameterTypes() != method.getGenericParameterTypes());
Field field = ...
assertTrue(field.getFieldType() == String.class);
assertTrue(field.getGenericFieldType() == Integer.class);
また、ランタイム例外がスローされるように、署名を無効として定義することもできます。この遅延は、情報が遅延評価されるときに初めてアクセスされたときにスローされます。(エラーのあるアノテーション値と同様です。)
特定のメソッドにのみパラメーターメタ情報を追加する
Javaコンパイラでは、parameter
フラグを有効にしてクラスをコンパイルするときに、パラメータ名と修飾子情報を埋め込むことができます。ただし、Javaクラスファイル形式では、この情報はメソッドごとに保存されるため、特定のメソッドのメソッド情報のみを埋め込むことができます。
混乱を招き、JVMをハードクラッシュさせる
例として、Javaバイトコードでは、任意の型の任意のメソッドを呼び出すように定義できます。通常、型がそのようなメソッドを知らない場合、検証者は文句を言うでしょう。ただし、アレイで不明なメソッドを呼び出すと、一部のJVMバージョンにバグが見つかりました。ベリファイアがこれを逃し、命令が呼び出されるとJVMが終了します。これはほとんど機能ではありませんが、技術的にはjavacでコンパイルされたJavaでは不可能です。Javaには、ある種の二重検証があります。最初の検証はJavaコンパイラーによって適用され、2番目の検証はクラスのロード時にJVMによって適用されます。コンパイラをスキップすると、ベリファイアの検証の弱点が見つかる場合があります。ただし、これは機能というよりは一般的な説明です。
外部クラスがない場合にコンストラクターのレシーバー型に注釈を付ける
Java 8以降、内部クラスの非静的メソッドおよびコンストラクターは、レシーバー型を宣言し、これらの型に注釈を付けることができます。トップレベルのクラスのコンストラクターは、レシーバータイプを宣言していないため、アノテーションを付けることはできません。
class Foo {
class Bar {
Bar(@TypeAnnotation Foo Foo.this) { }
}
Foo() { } // Must not declare a receiver type
}
以来、Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()
しかし戻りないAnnotatedType
表現をFoo
、ために型注釈を含めることが可能であるFoo
これらの注釈は、後リフレクションAPIによって読み出されたクラスファイルに直接のコンストラクタ。
未使用/レガシーバイトコード命令を使用する
他の人が名前を付けたので、それも含めます。Javaは以前、JSR
and RET
ステートメントによってサブルーチンを利用していました。JBCは、この目的のための独自のタイプの返信アドレスさえ知っていました。ただし、サブルーチンを使用すると、静的コード分析が複雑になりすぎたため、これらの命令は使用されなくなりました。代わりに、Javaコンパイラはコンパイルしたコードを複製します。ただし、これは基本的に同じロジックを作成するので、別のことを実現することはあまり考慮していません。同様に、たとえば、NOOP
Javaコンパイラーでも使用されないバイトコード命令ですが、これにより実際に何か新しいことを実現することはできません。コンテキストで指摘されているように、これらの言及された「機能命令」は、機能をさらに少なくする一連の正当なオペコードから削除されています。