Java言語では利用できないバイトコード機能 質問する

Java言語では利用できないバイトコード機能 質問する

現在 (Java 6)、Java 言語内からは実行できないが、Java バイトコードでは実行できることはありますか?

どちらもチューリング完全であることはわかっているので、「できる」を「大幅に高速/高速に、または単に異なる方法で実行できる」と読み替えてください。

私は のような追加のバイトコードを考えていますinvokedynamic。これは、特定のバイトコードを除いて、Java を使用して生成することはできませんが、将来のバージョン用です。

ベストアンサー1

かなり長い間 Java バイトコードを扱い、この問題についてさらに調査した結果、次のような結果が得られました。

スーパーコンストラクタまたは補助コンストラクタを呼び出す前にコンストラクタ内のコードを実行する

Java プログラミング言語 (JPL) では、コンストラクタの最初のステートメントは、スーパー コンストラクタまたは同じクラスの別のコンストラクタの呼び出しである必要があります。これは、Java バイト コード (JBC) には当てはまりません。バイト コード内では、次の条件を満たす限り、コンストラクタの前に任意のコードを実行することは完全に正当です。

  • このコード ブロックの後のある時点で、互換性のある別のコンストラクターが呼び出されます。
  • この呼び出しは条件文内にありません。
  • このコンストラクターの呼び出しの前に、構築されたインスタンスのフィールドは読み取られず、そのメソッドも呼び出されません。これは次の項目を意味します。

スーパーコンストラクタまたは補助コンストラクタを呼び出す前にインスタンスフィールドを設定する

前述のように、別のコンストラクタを呼び出す前にインスタンスのフィールド値を設定することは完全に合法です。Java バージョン 6 より前のバージョンでは、この「機能」を利用できるレガシー ハックも存在します。

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 のラムダ式をエミュレートすることもできます。Java 7 では、コンパイラーはまだ動的メソッド呼び出しの使用を許可していませんでしたが、JVM はすでに命令を理解していました。

通常は合法とはみなされない識別子を使用する

メソッド名にスペースや改行を使用したいと思ったことはありませんか? 独自の JBC を作成して、コード レビューの成功を祈ります。識別子に使用できない文字は、、、および のみです。.また、名前が付けられていないメソッドや、およびを含むことができないメソッドもあります。;[/<init><clinit><>

finalパラメータまたはthis参照を再割り当てする

finalパラメータは JBC には存在しないため、再割り当てが可能です。this参照を含むすべてのパラメータは、JVM 内の単純な配列にのみ保存され、単一のメソッド フレーム内のthisインデックスで参照を再割り当てできます0

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()、および 1 つの を受け取るコンストラクタを持つクラス ファイルを生成します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呼び出すように実装することが可能でした。直接のスーパークラスの実装とは別のスーパーメソッドの実装を呼び出すための明示的な呼び出しを定義することは依然として可能ですが、これは 1.1 以降の Java バージョンでは効果がありません。Java 1.1 では、この動作は、直接のスーパークラスの実装のみを呼び出す同じ動作を有効にするフラグを設定することによって制御されていました。Foo#bazBar#bazACC_SUPER

同じクラスで宣言されたメソッドの非仮想呼び出しを定義する

Javaではクラスを定義することはできない

class Foo {
  void foo() {
    bar();
  }
  void bar() { }
}

class Bar extends Foo {
  @Override void bar() {
    throw new RuntimeException();
  }
}

上記のコードは、 のインスタンスで が呼び出されるRuntimeExceptionと常に になります。を呼び出すメソッドを定義することはできません。fooBarFoo::foo独自の barで定義されている メソッドですFoo。 はbar非プライベート インスタンス メソッドであるため、呼び出しは常に仮想です。ただし、バイト コードを使用すると、のメソッド呼び出しをのバージョンにINVOKESPECIAL直接リンクするオペコードを使用するように呼び出しを定義できます。このオペコードは通常、スーパー メソッド呼び出しを実装するために使用されますが、オペコードを再利用して、説明されている動作を実装できます。barFoo::fooFoo

細粒度の型注釈

Java では、注釈は注釈が宣言する内容に従って適用されます@Target。バイト コード操作を使用すると、このコントロールとは独立して注釈を定義することができます。また、たとえば、注釈が両方の要素に適用される場合でも、パラメーターに注釈を付けずにパラメーター タイプに注釈を付けることができます@Target

型またはそのメンバーの属性を定義する

Java 言語では、フィールド、メソッド、またはクラスに対してのみアノテーションを定義できます。JBC では、基本的に任意の情報を Java クラスに埋め込むことができます。ただし、この情報を利用するには、Java クラスの読み込みメカニズムに頼ることはできず、メタ情報を自分で抽出する必要があります。

byteオーバーフローして、暗黙的に、、、shortおよび値charを割り当てるboolean

後者のプリミティブ型は、通常 JBC では認識されませんが、配列型またはフィールドおよびメソッド記述子に対してのみ定義されます。バイトコード命令内では、名前付き型はすべて 32 ビットのスペースを占め、 として表すことができますint。正式には、バイトコード内にはintfloatlong型のみdoubleが存在し、これらはすべて JVM の検証子の規則によって明示的な変換が必要です。

モニターをリリースしない

ブロックsynchronizedは実際には 2 つのステートメントで構成されます。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();
}

Java でブロックを使用する場合、モニターを解放する際に例外が発生すると、そのモニターを解放するための命令に戻るという同様のステートメントが暗黙的に作成されますsynchronized。通常、このような命令では例外は発生しませんが、発生した場合 (非推奨の などThreadDeath)、モニターは解放されます。

任意のデフォルトメソッドを呼び出す

Java コンパイラでは、デフォルト メソッドの呼び出しを許可するために、いくつかの条件を満たす必要があります。

  1. メソッドは最も具体的なものでなければなりません(実装されているサブインターフェースによってオーバーライドされてはいけません)どれでも型(スーパータイプを含む)
  2. デフォルト メソッドのインターフェース タイプは、デフォルト メソッドを呼び出すクラスによって直接実装される必要があります。ただし、インターフェースが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 が終了するというバグがあることがわかりました。これは機能とは言えませんが、技術的には不可能なことです。ジャバコンパイルされた 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()を返すため、 のコンストラクタの型注釈をクラス ファイルに直接含めることができ、これらの注釈は後でリフレクション API によって読み取られます。AnnotatedTypeFooFoo

未使用/レガシーバイトコード命令を使用する

他の人が名前を付けたので、これも含めます。Java は以前、 およびJSRステートメントによってサブルーチンを使用していRETました。JBC は、この目的のために独自の戻りアドレス タイプさえ認識していました。ただし、サブルーチンの使用は静的コード分析を過度に複雑にするため、これらの命令は使用されなくなりました。代わりに、Java コンパイラはコンパイルしたコードを複製します。ただし、これは基本的に同一のロジックを作成するため、私は実際に何か異なることを実現するとは考えていません。同様に、たとえば、NOOPJava コンパイラでも使用されないバイト コード命令を追加することもできますが、これによっても実際に何か新しいことを実現することはできません。コンテキストで指摘されているように、これらの「機能命令」は、現在、有効なオペコード セットから削除されているため、機能としてさらに劣っています。

おすすめ記事