現在、Java言語内ではできないJavaバイトコードでできること(Java 6)はありますか?
私は両方ともチューリングが完了していることを知っているので、「できる」と「大幅に速く/より良くできる、または単に別の方法でできる」と読みます。
Javaを使用して生成できないinvokedynamic
のような追加のバイトコードを考えていますが、特定のバイトコードは将来のバージョン用です。
私の知る限り、Java 6でサポートされているバイトコードには、Javaソースコードからもアクセスできない主要な機能はありません。これの主な理由は、明らかに、JavaバイトコードがJava言語を念頭に置いて設計されたことです。
ただし、最新のJavaコンパイラでは生成されない機能がいくつかあります。
これはクラスに設定できるフラグであり、invokespecial
バイトコードの特定のコーナーケースがこのクラスでどのように処理されるかを指定します。これはすべての最新のJavaコンパイラ(「modern」は> = Java 1.1、正確に覚えている場合)によって設定され、古代のJavaコンパイラのみが設定されていないクラスファイルを生成しました。このフラグは、後方互換性の理由でのみ存在します。 Java 7u51以降、ACC_SUPERはセキュリティ上の理由により完全に無視されることに注意してください。
jsr
/ret
バイトコード。
これらのバイトコードは、サブルーチンを実装するために使用されました(主にfinally
ブロックを実装するため)。それらは Java 6以降に生成されなくなった です。廃止される理由は、静的検証を大幅に複雑化するためです(つまり、使用するコードはほとんどのオーバーヘッドで通常のジャンプで再実装できます)。
クラス内に戻り型のみが異なる2つのメソッドがある。
Java言語仕様では、戻り型(つまり、同じ名前、同じ引数リスト、...)が異なるonlyの場合、同じクラスの2つのメソッドは許可されません。ただし、JVM仕様にはそのような制限がないため、クラスファイルcanにはそのようなメソッドが2つ含まれています。通常のJavaコンパイラを使用してこのようなクラスファイルを生成する方法はありません。 this answer に素敵な例/説明があります。
かなり長い間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>
または<clinit>
という名前のないメソッドには、<
および>
を含めることはできません。
final
パラメータまたはthis
参照を再割り当てします
final
パラメーターはJBCに存在しないため、再割り当てできます。 this
参照を含むすべてのパラメーターは、単一メソッドフレーム内のインデックス0
でthis
参照を再割り当てできるようにするJVM内の単純な配列にのみ格納されます。
final
フィールドの再割り当て
最終フィールドがコンストラクター内で割り当てられている限り、この値を再割り当てすることも、値をまったく割り当てないこともできます。したがって、次の2つのコンストラクターは有効です。
class Foo {
final int bar;
Foo() { } // bar == 0
Foo(Void v) { // bar == 2
bar = 1;
bar = 2;
}
}
static final
フィールドの場合、クラス初期化子の外部でフィールドを再割り当てすることもできます。
コンストラクターとクラス初期化子をメソッドであるかのように扱います
これは概念的な機能のようなものですが、JBC内ではコンストラクターは通常のメソッドと同じように扱われます。コンストラクターが別の正当なコンストラクターを呼び出すことを保証するのは、JVMの検証者だけです。それ以外では、コンストラクターは<init>
と呼ばれる必要があり、クラス初期化子は<clinit>
と呼ばれるというのは、単にJava命名規則です。この違いに加えて、メソッドとコンストラクターの表現は同じです。 Holgerがコメントで指摘したように、これらのメソッドを呼び出すことはできませんが、void
以外の戻り値の型を持つコンストラクター、または引数を持つクラス初期化子を定義することもできます。
スーパーメソッドを呼び出す(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
のインスタンスでfoo
が呼び出されると、上記のコードは常にBar
になります。 Foo::foo
メソッドを定義して、 its bar
で定義されているFoo
メソッドを呼び出すことはできません。 bar
は非プライベートインスタンスメソッドであるため、呼び出しは常に仮想です。ただし、バイトコードでは、Foo::foo
のINVOKESPECIAL
メソッド呼び出しをbar
のバージョンに直接リンクするFoo
オペコードを使用するように呼び出しを定義できます。このオペコードは通常、スーパーメソッドの呼び出しを実装するために使用されますが、記述された動作を実装するためにオペコードを再利用できます。
ファイングレイン型注釈
Javaでは、注釈が宣言する@Target
に従って注釈が適用されます。バイトコード操作を使用すると、このコントロールとは無関係に注釈を定義できます。また、たとえば、@Target
注釈が両方の要素に適用される場合でも、パラメーターに注釈を付けずにパラメータータイプに注釈を付けることができます。
型またはそのメンバーの属性を定義する
Java言語内では、フィールド、メソッド、またはクラスの注釈のみを定義できます。 JBCでは、基本的にJavaクラスに情報を埋め込むことができます。ただし、この情報を利用するには、Javaクラスローディングメカニズムに依存できなくなりますが、メタ情報を自分で抽出する必要があります。
オーバーフローして、byte
、short
、char
、boolean
の値を暗黙的に割り当てます
後者のプリミティブ型は、JBCでは通常知られていませんが、配列型またはフィールドおよびメソッド記述子に対してのみ定義されています。バイトコード命令内では、すべての名前付き型は32ビットのスペースを使用するため、int
として表現できます。公式には、int
、float
、long
、およびdouble
型のみがバイトコード内に存在し、これらはすべてJVM検証者の規則による明示的な変換が必要です。
モニターを解放しない
synchronized
ブロックは、実際には2つのステートメントで構成されています。1つはモニターを取得し、もう1つはモニターを解放します。 JBCでは、リリースせずに取得できます。
Note :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コンパイラーは、デフォルトのメソッドの呼び出しを許可するために、いくつかの条件を満たす必要があります。
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
- encoded値の一貫性をチェックしません。したがって、消去と一致しないジェネリック型に関する情報を定義することが可能です。結果として、次の主張が当てはまる場合があります。
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
およびRET
ステートメントによってサブルーチンを使用していました。 JBCは、この目的のために独自のタイプの返信先アドレスを知っていました。ただし、サブルーチンを使用すると、静的コード分析が複雑になり、これらの命令が使用されなくなった理由です。代わりに、Javaコンパイラーは、コンパイルするコードを複製します。ただし、これは基本的に同一のロジックを作成するため、実際には別の何かを達成するとは考えていません。同様に、たとえばJavaコンパイラーでも使用されないNOOP
バイトコード命令を追加することもできますが、これは実際には何か新しいことを実現することはできません。コンテキストで指摘されているように、これらの「機能の説明」は、機能をさらに少なくする法的オペコードのセットから削除されました。
JavaソースコードではなくJavaバイトコードで実行できる機能を次に示します。
メソッドがスローすることを宣言せずに、メソッドからチェック済み例外をスローします。チェック済みおよび未チェックの例外は、Java JVMではなくコンパイラ。このため、たとえばScalaはメソッドから宣言されていないチェック済み例外をスローできます。 Javaジェネリックでは sneaky throw と呼ばれる回避策があります。
戻り値の型のみが異なる2つのメソッドをクラスに持つ、Joachim's answer :Java言語仕様では、戻り型(つまり、同じ名前、同じ引数リスト、...)が異なるonlyの場合、同じクラスの2つのメソッドは許可されません。ただし、JVM仕様にはそのような制限がないため、クラスファイルcanにはそのようなメソッドが2つ含まれています。通常のJavaコンパイラを使用してこのようなクラスファイルを生成する方法はありません。 this answer に素敵な例/説明があります。
GOTO
をラベルとともに使用して、独自の制御構造を作成できます(for
while
などを除く)this
ローカル変数をオーバーライドできます関連するポイントとして、デバッグでコンパイルされた場合、メソッドのパラメーター名を取得できます( Paranamerはバイトコードを読み取ることでこれを行います
このドキュメント のセクション7Aは興味深いかもしれませんが、バイトコードfeaturesではなく、バイトコードpitfallsについてです。
Java言語では、コンストラクターの最初のステートメントはスーパークラスコンストラクターの呼び出しである必要があります。バイトコードにはこの制限はありません。代わりに、ルールは、メンバーにアクセスする前に、オブジェクトのスーパークラスコンストラクターまたは同じクラス内の別のコンストラクターを呼び出す必要があります。これにより、次のような自由度が増します。
これらはテストしていませんので、間違っている場合は修正してください。
プレーンJavaコードではなく、バイトコードでできることは、コンパイラなしでロードして実行できるコードを生成することです。多くのシステムには、JDKではなくJREがあります。コードを動的に生成する場合、Javaコードの代わりにバイトコードを生成する方が簡単ではありませんが、使用する前にコンパイルする必要があります。
Javaでは、パブリックメソッドを保護されたメソッド(またはその他のアクセス制限)でオーバーライドしようとすると、「弱いアクセス権限を割り当てようとしています」というエラーが表示されます。 JVMバイトコードを使用してこれを行う場合、ベリファイアはそれで問題なく、これらのメソッドをパブリッククラスのように親クラス経由で呼び出すことができます。
I-Playのときにバイトコードオプティマイザーを作成しました(J2MEアプリケーションのコードサイズを縮小するように設計されています)。追加した機能の1つに、インラインバイトコードを使用する機能がありました(C++のインラインアセンブリ言語に似ています)。値を2回必要とするため、DUP命令を使用して、ライブラリメソッドの一部である関数のサイズを小さくすることができました。また、ゼロバイト命令がありました(charを受け取るメソッドを呼び出してintを渡す場合、キャストする必要がないことがわかっているので、char(var)を置き換えるint2char(var)を追加すると削除されますコードのサイズを小さくするi2c命令。また、float a = 2.3; float b = 3.4; float c = a + b;を固定小数点に変換しました(より高速で、一部のJ2MEは浮動小数点をサポート)。