2025/04/01

バイトコード入門 その4 バイトコード処理の基礎

Java 24のリリース関連エントリーが続きましたが、再びバイトコード入門に戻ってきました。

今回はJVMスタックに積まれたフレームでどのようにバイトコードを処理するのか紹介していきます。今回は動作を説明するため、個々の命令についての説明は必要最低限とさせていただきます。

  1. 準備編
  2. スタックマシン
  3. バイトコード処理の構成
  4. バイトコード処理の基礎 (今回)

 

バイトコード処理構成のおさらい

まず、バイトコードを処理する構成について簡単におさらいしておきましょう。

バイトコードを実行するために、JVMスタックが使用されるということを前回説明しました。

JVMスタックはスレッドごとに作成されます。ここでのスレッドはOSのスレッドと1対1に対応するPlatform Threadを指しています。Virtual ThreadはPlatform Thread上で動作するので、Virtual Thread用にJVMスタックが作られることはありません。

JVMスタックには、メソッドコールごとにフレームが積まれます。

たとえば、mainメソッドからfooメソッドがコールされ、fooメソッドからbarメソッドがコールされて、barメソッドを処理している場合、JVMスタックにはmainメソッドのフレーム、fooメソッドのフレーム、barメソッドのフレームという3つのフレームが積まれます。

barメソッドの処理が完了し、fooメソッドに戻ってきた時点でbarメソッドのフレームもJVMスタックから取り除かれます。

 

フレームは主にローカル変数用配列と、バイトコード処理中のデータを保持するオペランドスタックから構成されます。

いずれもそのサイズはjavacコンパイル時に決定します。

 

バイトコード処理

今回は動作だけを説明するので、とても簡単なアプリケーションを使用しましょう。

Adderクラスは整数の足し算をするメソッドaddメソッドを持つクラスです。

public class Adder {
    public int add(int x, int y) {
        int z = x + y;
        return z;
    }

    public void static main(String... args) { 
        Adder adder = new Adder();
        int result = adder.add(2, 3);
        System.out.println(result);
    }
}

 

このプログラムが実行されると、メインスレッドに対応するJVMスタックが生成されます。そして、mainメソッドがコールされて、JVMスタックにもmainメソッドに対応するフレームが積まれます。

mainメソッドの中でaddメソッドがコールされると、JVMスタックにもaddメソッドに対応するフレームが積まれます。

この状態を表したのが、以下の図です。

 

では、addメソッド実行の様子を追っていきましょう。

 

addメソッドのバイトコード

Adderクラスをデバッグオプションの-gを使用してコンパイルし、javapでaddメソッドを調べたのが以下です(-gオプションについては、前回を参照してください)。

  private int add(int, int);
    descriptor: (II)I
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=2, locals=4, args_size=3
         0: iload_1
         1: iload_2
         2: iadd
         3: istore_3
         4: iload_3
         5: ireturn
      LineNumberTable:
        line 3: 0
        line 4: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   LAdder;
            0       6     1     x   I
            0       6     2     y   I
            4       2     3     z   I

ここで使用されているオペコード(命令)を簡単に説明しておきましょう。

  • iload: int型の整数をローカル変数配列からオペランドスタックに積む。_の後の数字はローカル変数配列のインデックス
  • iadd: int型の加算
  • istore: オペランドスタックの先頭にあるint型整数を取り出し、ローカル変数配列に格納。_の後の数字はローカル変数配列のインデックス
  • ireturn: オペランドスタックの先頭にあるint型整数を戻り値としてメソッドを完了させる

この後に実際の動きを説明するので、ここではそんなものぐらいに思っておいてください。

 

addメソッドの動作

では、オペランドスタックとローカル変数配列を含めて、addメソッドの動作を追っていきます。

addメソッドがコールされると、引数がローカル変数配列に格納されます。また、インスタンスメソッドの場合、暗黙の引数としてthisが渡され、それも一緒にローカル変数配列に置かれます。

addメソッドを引数としてx=2, y = 3でコールされた直後の様子は次のようになります。

 

 

ローカル変数配列のインデックス0にthis、1にxの値である2、2にyの値である3が入ります。

上図では分かりやすさのため、ローカル変数配列にx、y、zを記述してありますが、実際にはローカル変数名はコンパイル時に削除され、インデックスだけで扱われます。

ただし、前回説明したようにコンパイル時に-gオプションをつければ、LocalVariableTableが付随し、インデックスと変数名の対応が付けられるようになります。

 

はじめのiload_1はローカル変数配列のインデックス1の値をオペランドスタックに積むということです。iloadのiはint型を表しています。i以外にも接頭辞はありますが、それは次回説明することにします。

この結果、オペランドスタックには2が積まれます。

 

同様に、iload_2でオペランドスタックに3が積まれます。

 

次のiaddはint型の加算です。

ここでは、「バイトコード入門その2」で示したHPの電卓のように、演算に必要な個数だけ値をスタックから取り出し、結果をスタックに積みます。

加算の場合、演算に必要な値は2つなので、オペランドスタックに積まれていた3と2を取り出し、加算をした結果の5をオペランドスタックに積みます。

 

加算の結果はオペランドスタックにしかないので、オペランドスタックから値を取り出し、ローカル変数配列に収めるのが、次の行のistore_3です。

istore_3なので、配列のインデックス3の場所に加算結果の5を格納します。

この操作が、ローカル変数zに5を代入していることに相当します。

 

残りはJavaのコードのreturn z;の部分です。

iload_3で再び5をオペランドスタックに積みます。

 

そして、オペランドスタックに積んだ値を戻り値として、メソッドを完了させるのがireturnです。

ireturnが完了すると、addメソッドに相当するフレームは削除されます。

 

これでaddメソッドが完了しました。

このaddメソッドはとても単純なメソッドですが、オペランドスタックとローカル変数配列の使い方は変わらないので、これが理解できれば後はそれほど難しいことはないはずです。

ちなみに、最後のストアとロードが不要のように見えるかもしれませんが、気にしない方がよいです。

もし、このメソッドが何度もコールされるようであればHotSpot VMによって最適化されます。

javacでコンパイルした時点では最適化はされずに、本当に必要であれば実行時に最適化されるのです。

 

次回はメソッドコールやオブジェクト生成など、もうちょっと複雑なバイトコードを紹介していきます。また、if文やループなども紹介する予定です。

2025/03/18

JEPで語るJava 24

いつもはJEPで語れないだけですが、前回のエントリーで紹介したようにJava 24のAPIの変更はとても少なく、逆にJEPは24もあります。

そこで、今回はJava 24は24のJEPについて簡単に紹介していきます。

反応がよければ、シリーズ化するかも。

前回もリストアップしましたが、Java 24のJEPは以下の通り24になります。

  • 404: Generational Shenandoah (Experimental)
  • 450: Compact Object Headers (Experimental)
  • 472: Prepare to Restrict the Use of JNI
  • 475: Late Barrier Expansion for G1
  • 478: Key Derivation Function API (Preview)
  • 479: Remove the Windows 32-bit x86 Port
  • 483: Ahead-of-Time Class Loading & Linking
  • 484: Class-File API
  • 485: Stream Gatherers
  • 486: Permanently Disable the Security Manager
  • 487: Scoped Values (Fourth Preview)
  • 488: Primitive Types in Patterns, instanceof, and switch (Second Preview)
  • 489: Vector API (Ninth Incubator)
  • 490: ZGC: Remove the Non-Generational Mode
  • 491: Synchronize Virtual Threads without Pinning
  • 492: Flexible Constructor Bodies (Third Preview)
  • 493: Linking Run-Time Images without JMODs
  • 494: Module Import Declarations (Second Preview)
  • 495: Simple Source Files and Instance Main Methods (Fourth Preview)
  • 496: Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism
  • 497: Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm
  • 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe
  • 499: Structured Concurrency (Fourth Preview)
  • 501: Deprecate the 32-bit x86 Port for Removal

JEPで語れないシリーズでもそうですが、セキュリティ関連のJEP (JEP 478, JEP496, JEP 497)はさくらばがよく分かっていないので、省略します。

 

JEP 404 Generational Shenandoah (Experimental)

ShenandoahはRed Hatが中心となって作られているGCです。

Shenandoahはオブジェクトの世代を使用せずにGCを行うアルゴリズムでしたが、ZGCと同様に世代別GCを導入することになったようです。

Experimentalなのですぐに世代別GCを正式に導入するわけではないですが、次の次のLTSには世代別GCが正式になっていると思われます。

後ほど紹介しますが、ZGCは世代別GCだけを残すことになりました。今後、Shenandoahはどうするんでしょうね。

 

さて、世代別GCを使う方法です。

以下の実行時オプションを3つ指定します。

  • -XX:+UnlockExperimentalVMOptions
  • -XX:+UseShenandoahGC
  • -XX:ShenandoahGCMode=generational

ただし、Oracle OpenJDKやOracle JDKはShenandoah GCを含まないので、Red Hat JDKやEclipse Temurinなどを使ってみてください。

 

JEP 450 Compact Object Headers (Experimental)

Javaのオブジェクトはヒープに配置されますが、オブジェクトにはヘッダーがつきます。

オブジェクトヘッダーはJVMの実装依存の部分なのでJVMSには定義されていないのですが、HotSpot VMの場合ヘッダーに128bit使用します。

しかし、小さいクラスだとヘッダーがバカになりません。

たとえば、record Point(int x, int y) {} なんていうクラスだと、データとしては8byte (64bit)しかありません。こうなると、ヘッダーの方が大きくなってしまうわけです。

そこで、現状128bitあるオブジェクトヘッダーを小さくするために立ち上がったのがProject Lilliputです。

Project LeadはAmazonのRoman Kennkeさん。数少ないOracleがリードではないプロジェクトです。

彼はJVMLSでProject Lilliputの講演をしているのですが、Lilliputの背景や概要についてはJVMLS 2023の講演が参考になると思います。

 

ちなみに、プロジェクト名のLilliputですが、ガリバー旅行記の出てくる小人の国のリリパット王国のことです。

なかなかいいプロジェクト名だと思いませんか?

 

Project Lilliputは、64bit VMと32bit VMの両方に対応していますが、ここでは64bit VMについて説明します。

オブジェクトヘッダーには以下のような情報が保持されます。

  • GC Age
  • 型(クラス)
  • ロック
  • ハッシュコード

GC Ageというのは、オブジェクトがGCから生き残ってきた回数を表します。世代別GCの場合、このAgeによってオブジェクトをYoung領域からOld領域に移動させます。

64bit VMではオブジェクトヘッダーが128bitで、上位64bitと下位64bitに分割されて使用されます。

上位64bitはマークワードと呼ばれ、ハッシュコード、GC Age、ロック情報が格納されます。ロック情報は下図のTagビットで表されます。

Mark Word (normal):
 64                     39                              8    3  0
  [.......................HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH.AAAA.TT]
         (Unused)                      (Hash Code)     (GC Age)(Tag)

 

下位64bitはクラスワードと呼ばれ、クラスポインターが格納されます。

Class Word (uncompressed):
64                                                               0
 [cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc]
                          (Class Pointer)

 

これに対し、Project Lilliputでは以下のようにヘッダーを64bitに抑えます。

Header (compact):
64                    42                             11   7   3  0
 [CCCCCCCCCCCCCCCCCCCCCCHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHVVVVAAAASTT]
 (Compressed Class Pointer)       (Hash Code)         /(GC Age)^(Tag)
                              (Valhalla-reserved bits) (Self Forwarded Tag)

 

これが実現されれば、多量のオブジェクトを使うシステムではかなりヒープ使用量が減るはずです。

ただし、ちょっと分からない部分もあります。上図のValhalla-reserved bitsです。

これはValue Class用のビットです。Value Objectはヒープの平坦化がされれば、オブジェクトヘッダーを使用しません。しかし、平坦化されない場合は通常のオブジェクトと同様にヘッダーを使用します。

この場合、通常のオブジェクトとValue Objectを区別するために上記のビットが使われることになると思われます。

Value Classが正式化されるまで、Previewのままなのか、それとも見切り発車で進んでしまうのか、どちらなんでしょうね。

 

Compact Object Headerを使用するには以下の2つの実行時オプションを使用します。

  • -XX:+UnlockExperimentalVMOptions
  • -XX:+UseCompactObjectHeaders

 

472: Prepare to Restrict the Use of JNI
498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe

最近、OpenJDKではIntegrityがトピックになっています。

Integrityの意味は「誠実さ」とか「正直さ」などです。JavaでIntegrityってよく分からないですよね。

JavaのAPIや機能、ツールなどには黒魔術とも呼べるような安全ではない使い方ができるものがあります。

しかし、安全で堅牢なシステムを作成するにあたっては、このような機能は徐々に取り除いていかないとダメだよねというのが、JEP draft: Integrity by Defaultです。

また、このJEPの背景を語るドキュメントとしてPeaceful and Bright Future of Integrity by Default in Javaがあります。ありがたいことに、このドキュメントは西川さんが翻訳してくれています。

このJEPはまだdraftなので今後どうなるか分からないのですが、Java 24でもIntegrity by Defaultに関連したJEPがあります。それが、JEP 472とJEP 498です。

JEP 472がJNIの使用を制限するJEPで、JEP 498がsun.misc.Unsafeの使用を制限するというJEPです。

特にUnsafeはいろいろと危険なことができてしまっていたのですが、徐々に機能が減らされていって、最後に残っていたのがネイティブメモリへのアクセスだったのです。これに対し、安全にメモリにアクセス可能なFFMが提供されたので、ようやくUnsafeがお役御免となったわけです。まだ、Unsafeを使用すると警告が出るだけですが、だんだんと使えなくなっていくはずです。

 

475: Late Barrier Expansion for G1

G1GCの実装を改善しましょうというJEP。

バリアは何かしらを守るために使用される、同期方法の一種です。たとえば、CPUでメモリ操作の順序性を保証するために使われるメモリバリアなどがあります。

G1GCでもプリライトバリアやポストライトバリアなどのバリアが使われるのですが、そのバリアの処理が重いので、特にJITのC2コンパイラ使用時に改善していきましょうという提案です。

これはStandard JEPで、特に指定しなくてもG1GCを使用時には適用されます。

このJEPに関してThomas Schatzlが解説を書いてくれています。ありがたいことに、西川さんが翻訳してくれています。

 

479: Remove the Windows 32-bit x86 Port
486: Permanently Disable the Security Manager
501: Deprecate the 32-bit x86 Port for Removal

Integrity by Defaultとは関係ないのですが、機能を削除する関連のJEPがJEP 479、JEP 486、JEP 501です。

JEP 479とJEP 501は32bitのx86のポートを削除するJEPで、JEP 486が使わなくなったSecurity Managerを削除するJEPです。

 

483: Ahead-of-Time Class Loading & Linking

Javaは起動時間が遅いとよく言われますが、それを改善するためのプロジェクトがProject Leydenです。

Leydenとはライデン瓶のライデンですね。ライデン瓶は一種のコンデンサーで、導通した時に一気に電気を流せることからプロジェクト名になったんだと思います。あくまでも、櫻庭の予想ですが...

さて、Project LeydenではAOTコンパイラーが提供される予定ですが、その前にJEP 483で事前クラスローディングやリンクを可能とします。

 

Javaの起動時には様々な処理が行われます。その中でも、クラスロードし、クラスの解析、リンク、staticの初期化までの処理はシステムが大規模になればなるほど長い時間を必要とします。また、システムによっては、実行時アノテーションの解決も必要になります。

しかし、これらの処理は毎回同じことを繰り返すだけなので、その部分を事前にやってしまおうというのがJEP 483です。

実際には、トレーニング実行でこれらの処理をキャッシュとして保存しておき、本番時にはキャッシュを使用してシステムを起動します。

キャッシュファイルをapp.aotconfとした場合、以下のようにトレーニング実行、キャッシュファイルの保存という2段階でキャッシュファイルを作成します。

  • トレーニング実行
    $ java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -cp app.jar com.example.App ...
  • キャッシュファイル作成
    $ java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot -cp app.jar

2番目のキャッシュファイル作成時にはアプリケーションは実行しません。

キャッシュファイルができたら、それを使用して実行します。

  • 本番実行
    $ java -XX:AOTCache=app.aot -cp app.jar com.example.App ...

 

484: Class-File API

Class-File APIはバイトコード操作のためのAPIです。

クラスファイルを解析したり、バイトコードで記述して直接クラスファイルを生成したりすることができます。

JDKの内部では動的にクラスを生成する場合などにバイトコード操作が行われてきました。この時に使用されていたのが、ASMです。

では、なぜ今になってASMではなくて、自前のバイトコード操作APIを作成することになったのでしょう。

クラスファイルにもバージョンがあり、バージョンが上がるごとに記述できる情報が増えていっています。しかし、サードパーティーのASMだと、最新のバージョンをサポートするまでに時間がかかってしまいまいます。

新しいクラスファイルをすぐにサポートするためには、やはり自前でバイトコード操作APIを作らないとダメということのようです。

 

Class-File APIでは、クラスファイルの読み、書き、改変をサポートしています。また、ストリーミング的な使い方と、イベント的な使い方の両方ができるようになっています。XMLでいうところのSAXとDOMのような使い分けができるはずです。

 

Class-File APIに関しては、今続けているバイトコード入門の続きとして紹介する予定です。

また、JJUGのナイトセミナーで少しだけ紹介したので、参考までに資料を張っておきます。

 

485: Stream Gatherers

Stream APIの中間操作をカスタマイズできるようにするのがStream Gathererです。

Stream Gathererに関しては、すでに解説エントリーを書いているので、そちらを参照してみてください。

 

487: Scoped Values (Fourth Preview)

Project Loomで策定されているAPIの1つであるScoped Valueはスレッド間でイミュータブルなデータを共有するための機能です。

今まで使用してきたThreadLocalはいろいろと問題があるので、それを全部ではないですけど、ある程度置き換えられる機能になっています。

4th PreviewでなかなかStandardにならないですが、次のJava 25でStandardになればいいかなという感じですね。

Java 24では1つだけメソッドが削除されて、一貫性のある使い方に整理されたようです。

 

488: Primitive Types in Patterns, instanceof, and switch (Second Preview)

パターンマッチングにプリミティブ型を使用できるようにしようというのがJEP 488です。

これのおもしろいのが、プリミティブ型の値とのマッチングと、型とのマッチングを同居させることができるところです。たとえば、こういう記述ができます。

switch(x) {
    case 0 -> System.out.println("Zero");
    case 1 -> System.out.println("One");
    case int i when i > 100 -> System.out.println("Big int: " + i);
    case int i -> System.out.println("Small int: " + i);
}

いろいろとルールはあるので、細かいところはJEP 488を読んでみてください。

 

489: Vector API (Ninth Incubator)

Javaでベクター処理を行うためのVector APIですが、Value Classが導入されるまではずっとインキュベータのままということになっています。

 

490: ZGC: Remove the Non-Generational Mode

ZGCはJava 23で世代別ZGCがデフォルトになりましたが、そうそうに元々の非世代別ZGCが削除されることになりました。

世代別と非世代別の両方をサポートするのは大変なのは分かりますが、そこまで急いで削除しなくてもいいのではと思ってしまいます。

 

491: Synchronize Virtual Threads without Pinning

Virtual Threadの使用時にsynchronizedを使うと、Virtual Threadを実行するキャリアスレッドをブロックしてしまうため性能が落ちるという問題がありました。

そのため、Virtual Threadを使う時にはsynchronizedではなく、ReentrantLockを使うようにしましょうというのが今までの解決法でした。

これに対し、synchronizedを使ってもキャリアスレッドをブロックしないようにするのがJEP 491です。

とはいえ、JEP 491が導入されれば、Virtual Threadでもsynchronizedを書き放題と思うのは早計です。

synchnronizedでもReentrantLockでも同期をするためにVirtual Threadをブロックします。キャリアスレッドのブロックよりはいいかもしれませんが、ブロックはブロックです。

Virtual Threadを使うようなスケールでは、些細なブロックでもできれば避けた方が賢明です。

ライブラリやフレームワークでsynchornizedを使用しているため、今までVirtual Threadを使っていても性能が出なかったという場合であればよいのですが、新たにVirtual Threadを使うコードを記述するのであれば、なるべくスレッドを独立にしてブロックしないような設計にするのがよいと思います。

 

492: Flexible Constructor Bodies (Third Preview)

コンストラクター内でスーパークラスや自分自身のコンストラクターをコールするのは、コンストラクターの先頭と決まっていました。

これに対し、フィールドの初期化の後にも書けるようにしたのがJEP 492です。

なぜこんなことが必要なのかというのと、Project Valhallaが関係しています。

Project ValhallaではValue Classの導入と、その効率化のためにNull非許容な型が導入されます。

しかし、コンストラクター内でスーパークラスのコンストラクターを先頭でコールしてしまうと、フィールドが初期化されていない状態でスーパークラスから参照できてしまいます。これはNull非許容の場合だと問題になります。

そのため、Value ClassやNull非許容型が導入される前に、JEP 492で問題となりそうな箇所をつぶしておこうというわけです。

Java 23の3rd Previewからの変更点はないので、Java 25ではStandardになるはずです。

 

493: Linking Run-Time Images without JMODs

JDKの標準ライブラリーはモジュール構成になっており、JARではなくJMODで提供されています。

当然、jlinkでランタイムを作成する場合もJMODがそのまま使われていました。これに対し、モジュールのJARでも可能にするようにしたのがJEP 493です。

 

494: Module Import Declarations (Second Preview)

import文にモジュール単位で記述できるようにするのがJEP 494です。

import module java.base;と書いておけば、java.langパッケージやjava.utilパッケージのクラスやインタフェースのimport文を書かずに済みます。

モジュール間で同じクラス名を使用している場合、明示的に優先的に使用するクラスのimport文を記述します。

たとえば、java.baseとjava.desktopをインポートしてしまうと、java.util.Listとjava.awt.Listなどの同じ名前のクラス/インタフェースをインポートしてしまいます。java.util.Listを優先的に使うのであれば、次のように記述します。

import module java.base;
import module java.base;

import java.util.List;

 

495: Simple Source Files and Instance Main Methods (Fourth Preview)

JEP 495は、mainメソッドの記述を簡素化するためのJEPです。

mainメソッドのためのクラスを書く必要がなくなり、void main() { ... }だけでOKになります。

また、標準出力への出力もSystem.out.println(...);ではなく、println(...);だけでよくなります。

このJEPもなかなかStandardになりませんが、Java 24での変更点はないので、このままJava 25でStandardになると予想されます。

 

499: Structured Concurrency (Fourth Preview)

JEP 499はProject Loomで策定されている仕様の1つです。

複数のスレッドの結果をまとめるためのAPIで、すべての処理結果を待つことや、失敗が1つでもあったら処理を失敗とするなどといったことが簡単に記述できます。

今までであれば、CompletableFutureを使えば同じようなことを記述できるのですが、Thread単体だとちょっと面倒でした。

そこで、Virtual Threadの導入とともにStructured Concurrencyが導入されるはずだったのですが、なかなかStandardにならないまま...

しかし、Java 24での変更はないので、Java 25でStandardになるのではないかと思うのですが、どうでしょう。

 

まとめ

というわけで、セキュリティ関連を除いてJava 24のJEPを簡単に紹介してきました。

最後の方はかなり簡単な紹介だけになってしまいましたが...

PreviewやIncubatorのJEPはStandardになった時に改めて紹介したいと思います。

2025/03/17

JEPでは語れないJava 24

毎度おなじみ半年ぶりのJavaのアップデートです。

Java 24はLTSの1つ前のバージョンだからか、怒涛のJEP祭の様相を呈しています。

なんとJEPが24!!Standard JEPに限っても14もあります。こんなにJEPが多いのはJava 9以来はじめてです!

Java 24のJEPは以下の通り。

  • 404: Generational Shenandoah (Experimental)
  • 450: Compact Object Headers (Experimental)
  • 472: Prepare to Restrict the Use of JNI
  • 475: Late Barrier Expansion for G1
  • 478: Key Derivation Function API (Preview)
  • 479: Remove the Windows 32-bit x86 Port
  • 483: Ahead-of-Time Class Loading & Linking
  • 484: Class-File API
  • 485: Stream Gatherers
  • 486: Permanently Disable the Security Manager
  • 487: Scoped Values (Fourth Preview)
  • 488: Primitive Types in Patterns, instanceof, and switch (Second Preview)
  • 489: Vector API (Ninth Incubator)
  • 490: ZGC: Remove the Non-Generational Mode
  • 491: Synchronize Virtual Threads without Pinning
  • 492: Flexible Constructor Bodies (Third Preview)
  • 493: Linking Run-Time Images without JMODs
  • 494: Module Import Declarations (Second Preview)
  • 495: Simple Source Files and Instance Main Methods (Fourth Preview)
  • 496: Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism
  • 497: Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm
  • 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe
  • 499: Structured Concurrency (Fourth Preview)
  • 501: Deprecate the 32-bit x86 Port for Removal

 

こんなにJEPがあるのですが、なんとAPIの変更はとてもわずかしかないのです。

JEP 484 Class-File APIとJEP 485 Stream GatherersによるAPI追加以外はほとんどありません。

しかたないので、今回に限ってJEPを語るにしてしまいましょう。本エントリーではいつも通りJEPで語らない部分を紹介し、次のエントリーでJEPについて紹介していきます。

なお、java.baseモジュール以外だと、java.desktopモジュールに変更がありますが、変更点が少ないので省略します。また、いつも通り、セキュリティ関連も省略します。

 

廃止になったAPI

Java 24で削除されたAPIは2つだけです。いずれもフィールドです。

フィールド

削除されたフィールドはすでに削除された機能に関連しているフィールドです。

  • java.awt.Window.warningString
  • javax.naming.Context.APPLET

1つ目のWindowクラスのwarningStringはpublicなフィールドではありません。

この文字列はgetWarningString()メソッドで返される文字列でした。セキュリティマネージャで設定されるパーミッションが設定されている場合はnullが返るようになっていたのですが、セキュリティマネージャが廃止されたため、常にnullを返すようになっています。

このため、getWarningString()メソッドで返す文字列が必要なくなったため、削除されました。

 

2つ目のContextクラスのAPPLETは、まぁしかたないですね。APPLETはなくなってしまいましたし。

 

廃止予定に追加されたAPI

Java 24では3つのクラスと2つのメソッドが廃止予定に追加されました。

クラス

  • java.awt.AWTPermission
  • java.util.zip.ZipError
  • javax.sound.sampled.AudioPermission

AWTPermissionクラスとAudioPermissionクラスはWindow.warningStringと同様にセキュリティマネージャが削除されたためです。

ZipError例外はエラー出ないZipExcpeion例外があるので、もはや使われていない例外のため削除予定です。

 

メソッド

  • java.awt.Window.getWarningString()
  • java.swing.JInternalString.getWarningString()

どちらもセキュリティマネージャが削除されたためで、現状はnullを返すだけになっています。

 

追加/変更されたAPI

いつもの通り、Preview JEPに関するAPI変更はここでは省略します。また、セキュリティ関連も省略します。

 

java.base/java.ioパッケージ

JEP 495で導入予定のjava.io.IOクラスに関連してConsoleクラスに2つのメソッドが追加されました。Java 23でもメソッドが追加されていましたが、追加の追加という感じ。

IOクラス自身はプレビューなので、Starndard JEPになったら説明します。まぁ、大したことではないんですけどね。

Consoleクラス

Consoleクラスには2つのメソッドが追加されました。

  • Console println()
  • Console readln()

printlnメソッドは改行を出力で、readlinメソッドは1行読み込みです。

いずれも引数があるオーバーロードがJava 23で導入されていたので、おまけのような感じ。

とはいっても、Java 23とJava 24で追加されたメソッド群はまだPreviewなので、使うには--enable-previewオプションが必要です。

 

Readerクラス

Readerクラスにファクトリメソッドが追加されました。

Reader/Writerクラスはアブストラクトクラスで、基本的にはデコレーターパターンで機能を付け加えるという使い方をしていました。しかし、ファクトリーメソッドができたということは、今後Readerクラスの使い方が変わるかもしれません。

  • static Reader of(CharSequence cs)

ofメソッドでは、引数で指定したCharSequenceオブジェクトから読み込んでいくReaderオブジェクトを生成します。

動作としては、StringReaderクラスとほぼ同じです。

 

java.base/java.langパッケージ

Java 24は2024年にリリースされたUnicode 16.0をサポートしました。これに対応して、文字関連のAPIが追加されています。

Charater.UnicodeBlockクラス

Unicode 16.0では新たに絵文字が追加されたり、ヒエログリフが追加されたことが話題になりましたね。

これに応じて、UncodeBlockクラスでは新たに10のブロックに対応する定数が追加されています。

  • static final UnicodeBlock EGYPTIAN_HIEROGLYPHS_EXTENDED_A
  • static final UnicodeBlock GARAY
  • static final UnicodeBlock GURUNG_KHEMA
  • static final UnicodeBlock KIRAT_RAI
  • static final UnicodeBlock MYANMAR_EXTENDED_C
  • static final UnicodeBlock OL_ONAL
  • static final UnicodeBlock SUNUWAR
  • static final UnicodeBlock SYMBOLS_FOR_LEGACY_COMPUTING_SUPPLEMENT
  • static final UnicodeBlock TODHRI
  • static final UnicodeBlock TULU_TIGALARI

 

Charater.UnicodeScript列挙型

UncodeScript列挙型でもUnicode 16.0に対応するため、新たに7つの定数が追加されています。

  • GARAY
  • GURUNG_KHEMA
  • KIRAT_RAI
  • OL_ONAL
  • SUNUWAR
  • TODHRI
  • TULU_TIGALARI

 

Processクラス

メソッドが1つだけオーバーロードされました。

  • boolean waitFor(java.time.Duration duration)

waitForメソッドはプロセスの終了を待つというメソッドです。

Java 23までは引数なしで終了を待つものと、タイムアウトをlongとTimeUnit列挙型で指定する2つのオーバーロードがありました。

Java 24では、これらに加えてタイムアウトをDate & Time APIのDurationクラスで指定するものが加わっています。

 

java.base/java.lang.classfileパッケージ

java.lang.classfileパッケージとそのサブパッケージはJEP 484 Class-File APIで使用するパッケージです。

簡単な紹介は次回の「JEPで語る」でしますが、詳細な説明は「バイトコード入門」の続編として書く予定です。

 

java.base/java.lang.reflectパッケージ

java.lang.reflectパッケージはいつも通り新しいバージョンに合わせた定数が追加されています。

ClassFileFormatVersion列挙型

いつものように、Java 24に対応する定数が追加されました。

  • RELEASE_24

 

java.base/java.util.streamパッケージ

JEP 485 Stream Gatherersによって、インタフェースとクラスが追加されています。

Gathererインタフェース

中間操作のためのインタフェースが追加されました。Gathererインタフェースを使うと、終端操作のCollectorインタフェース相当の機能が中間操作で実現できます。

 

Gatherersクラス

Gathererインタフェース用のユーティリティクラスです。

Stream Gathererの使い方は、本ブログの Stream Gatherer 基礎編 を参照してください。

 

Streamインタフェース

StreamインタフェースにはGathererを使うためのメソッドが追加されました。

  • <R> Stream<R> gather(Gatherer<? super T, ?, R> gatherer)

Stream Gathererの使い方は、本ブログの Stream Gatherer 基礎編 を参照してください。

 

おわりに

なんとAPIの変更はこれだけ!!

追加されたAPIとしては、Class-File APIがあるのでむちゃくちゃ多いのですが、それ以外はほんのわずか。Java 24はAPIの変更よりも、次のLTSのために不要な実装を削除するなどの整理が主になっている感じです。

また、プリミティブ型のパターンマッチングなどの文法の変更も次のLTSでStandard JEPになることが予想されます。

 

さて、次のエントリーではJEPに関して簡単な説明を加えていきます。

2025/03/06

バイトコード入門 その3 バイトコード処理の構成

ちょっと間があいてしまいましたが、バイトコード入門の3回目です。今回はバイトコードをどのように処理してJVMの構成について紹介します。前回のスタックマシンがここで活かされてきます。

  1. 準備編
  2. スタックマシン
  3. バイトコード処理の構成 (今回)

 

JVMのメモリ構成

JVMで管理しているメモリ領域というと、多くの方がヒープと考えるのではないでしょうか。たしかに、ヒープは重要なメモリ領域であることには違いはありませんが、ヒープ以外にもJVMが管理しているメモリ領域があるのです。

HotSpot VMの場合、JVMが管理しているメモリ領域は大別して4種類あります。

  • ヒープ: オブジェクトの配置用の領域
  • メタスペース: クラス定義、メソッド定義、コンスタントプールなど
  • ネイティブメソッドスタック: ネイティブメソッド用領域
  • JVMスタック: コールスタック用領域

JVMSはJVMの仕様については記述されていますが、実装については記述されません。メモリ構成も実装に近いため大まかにしか説明されていませんが、JVMS 2.5 Run-Time Data Areasに記述があります。

なお、下図はJVMスタックを省略しています。

 

1つ目のヒープは、一番なじみがあるメモリ領域だと思います。

Javaのオブジェクトは必ずこのヒープに配置されます。GCの種類によってヒープはさらに細分化されるのですが、それはGCによるものでJVMSでは定義されていません。

たとえば、世代別GCを使用している場合、Young領域とOld領域に分けられるなどがこれに相当します。

2番目のメタスペースは静的なデータを保持させる領域で、クラスローダーごとに管理されています。

メタスペースというのはHotSpot VMの実装に基づいた領域名で、メタスペースに保存するデータとしてはクラス定義、メソッド定義などがあります。また、コンスタントプールもメタスペースに保持されます。

JVMSでは2.5.4 Method Area2.5.5 Run-Time Constant Poolがメタスペースの一部になっています。

ネイティブメソッドスタックは、JNIやFFMでネイティブメソッドを使用する際に使われる領域です。

最後のJVMスタック領域が今回メインで取り上げるメモリ領域です。JVMSでは2.5.2 Java Virtual Machine Stacksで定義されています。

これ以外にスレッドごとにどこを実行しているかを保持しておくPCレジスタ(Program Counter)もあります(JVMS 2.5.1)。

 

JVMスタック領域

例外がスローされた時に出力されるスタックトレースはJavaの開発者であれば誰もが見たことがあるはずですが、その意味を考えたことがありますか?

何かのスタックをトレースしたものということは分かると思います。そのスタックというのは、メソッドがコールされた順序を保持しておくスタックを指しています。一般的にはコールスタックと呼ばれるスタックです。

Javaの場合、このコールスタックはJVMスタックと呼ばれます。そして、そのJVMスタックが配置される領域がJVMスタック領域です。

なお、コールスタックが使用されていても、前回紹介したスタックマシンとは呼ばれないことに注意が必要です。

さて、JavaのJVMスタックは、並列処理が可能なようにスレッドごとに作られます。

そして、JVMスタックに積まれるのがフレームです(まちがえないとは思いますが、AWTのjava.awt.Frameクラスではないです)。

フレームはメソッドコールごとにJVMスタックに積まれます。たとえば、以下のようにmainからfoo、barとコールされる場合を考えてみましょう。

public class Main {
    static void bar() {}

    static void foo() {	bar(); }
    
    public static void main(String... args) {
	foo();
    }
}

Mainクラスを実行すると、まずmainに対応するフレームが積まれます。fooメソッドがコールされるとそれに対応するフレームが積まれます。

そして、fooメソッドからbarメソッドがコールされると、barメソッドに対応するフレームが積まれます。

barメソッドが完了して、fooメソッドに戻る時に、barメソッドに対応するフレームは削除されます。

同様にfooメソッドの完了時にfooメソッドに対応するフレームが削除され、mainメソッドが完了する時にmainメソッドに対応するフレームが削除され、JVMスタックは空になります。

 

ところで、StackOverflowErrorという例外に遭遇したことがあるでしょうか。再帰などでコードにバグがある時に遭遇することが多い例外です。

再帰では自分自身を延々とメソッドコールするわけですが、メソッドコールの連なりが限度を超えた場合にStackOverflowError例外がスローされます。

もうお分かりだとは思いますが、StackOverflowError例外のスタックとはJVMスタックのことです。

JVMスタックにフレームを積み過ぎてあふれてしまうと、StackOverflowError例外がスローされるのです。

同様に例外発生時に提示されるスタックトレースのスタックもJVMスタックです。

例外発生時に、どのメソッドのコールされていたかは、JVMスタックをたどれば分かります。これがスタックトレースです。

実際には、次節で紹介するJVMスタックに積まれるフレームの情報を含めてスタックトレースが作られます。

 

フレーム

JVMスタックに積まれるフレームはJVMS 2.6 Framesで定義されています。

フレームの主要な構成要素は以下の2つです。

  • ローカル変数用配列
  • オペランドスタック

 

ローカル変数用配列

1つ目のローカル変数用配列は、ローカル変数とメソッドの引数を保持させる配列です。

ローカル変数およびメソッド引数がいくつ使用するのかは、ソースコードをコンパイルする時に調べることができます。このため、配列の要素数はその個数分になります。

ローカル変数もしくは引数がプリミティブ型の場合、その値が直接配列に保持されます。参照型の場合はその参照が保持されます。

また、インスタンスメソッドの場合、インデックス0には必ずthisが入ります。

 

とこで、ローカル変数/メソッド引数の名前はコンパイルすると情報として残りません。メソッド内では、ローカル変数用配列のインデックスで指定されます。

しかし、これだとクラスファイルを読んだだけだと何が配列に入っているのかが分かりにくいんですよね。

こんな理解度の低い人間のためのことを、javacはちゃんと用意してくれてあります。それがコンパイルオプションの-gです。

-gはデバッグ情報をクラスファイルに埋め込むためのオプションです。

たとえば、次のメソッドで試してみましょう。

 

    void sayHello(String name) {
        var text = "Hello, " + name + "!";
        System.out.println(text);
    }

 

これを-gを使用せずにコンパイルし、javap -vで表示させると次のようになります。

 

  void sayHello(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0000)
    Code:
      stack=2, locals=3, args_size=2
         0: aload_1
         1: invokedynamic #7,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
         6: astore_2
         7: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: aload_2
        11: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: return
      LineNumberTable:
        line 3: 0
        line 4: 7
        line 5: 14

 

赤字で示したlocal=3がローカル変数用配列のサイズになります。

そして、次回、詳しく説明しますが、aload_1やastore_2の1や2が配列のインデックスになります。

しかし、そのインデックス1や2に何が保持されているかは、バイトコードから推測するしかありません。

 

そこで-gを使用してコンパイルしてみます。コンパイル結果を次に示します。

 

  void sayHello(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0000)
    Code:
      stack=2, locals=3, args_size=2
         0: aload_1
         1: invokedynamic #7,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
         6: astore_2
         7: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: aload_2
        11: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: return
      LineNumberTable:
        line 3: 0
        line 4: 7
        line 5: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   LHello;
            0      15     1  name   Ljava/lang/String;
            7       8     2  text   Ljava/lang/String;

 

-gオプションを使用することで、最後にLocalVariableTableという表が追加されました。

この表のSlotが配列のインデックスになります。

sayHelloメソッドはインスタンスメソッドなので、前述したようにインデックス0にはthisが入ります。

インデックス1には引数のname、インデックス2にはローカル変数のtextです。

 

クラスファイルを読み慣れてくれば、Local Variable Tableがなくても、配列のどこに何が保持されているかは分かってきます。しかし、慣れないうちは、コンパイルオプションの-gをつけてコンパイルすることをお勧めします。

 

オペランドスタック

フレームのもう一方の構成要素がオペランドスタックです。名前の通り、オペランドを保持させるスタックです。

もちろん、このスタックがJVMをスタックマシンたらしめているスタックです。

オペランドというのは、処理命令であるオペコードの処理対象のデータのことになります。

バイトコードにはloadやstoreといったオペコードがあります。これらはローカル変数用配列からスタックにデータを積む、スタックの先頭データを取り出してローカル変数用配列に保持させるというようにスタックに対する処理になります。

他のバイトコードもほとんどがスタックやスタックのデータに対する命令となります。

 

このスタックのサイズも、コンパイル時に決まります。

先ほどのsayHelloメソッドの場合を見てみましょう。sayHelloメソッドの先頭部分を再掲します。

 

  void sayHello(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0000)
    Code:
      stack=2, locals=3, args_size=2

このstack=2がオペランドスタックのサイズとなります。

 

ちなみに、args_size、つまり引数の個数が2になっているのは、インスタンスメソッドの暗黙の引数としてthisが渡されるからです。

 

バイトコード処理構成のまとめ

長くなってきたので、今回はここまでとして、まとめてみましょう。

  • JVMのメモリ領域のうち、バイトコード処理に使われるのはJVMスタック領域
  • JVMスタックはスレッドごとに作成され、メソッドコールごとにフレームが積まれる
  • フレームはローカル変数用配列とオペランドスタックなどから構成される
  • オペランドスタックを使用してバイトコードの処理を行う

 

たぶん、次のエントリーはJEPで語れないシリーズのJava 24になると思うので、その後のエントリーでやっとバイトコード処理について解説できるはずです。

2025/02/12

バイトコード入門 その2 スタックマシン

前回からはじめたバイトコード入門。なかなかバイトコードに入れなくても申し訳ないのですが、今回はスタックマシンについて紹介します。

  1. 準備編 (前回)
  2. スタックマシン (今回)
  3. バイトコード処理の構成

 

スタックマシン

スタックマシンというのは計算モデルの1つです。

スタックマシンと、よく引き合いに出される計算モデルにレジスタマシンがあります。

この2つの計算モデルはメモリーの使い方にあります。

  • スタックマシン: メモリーをスタックとする計算モデル
  • レジスタマシン: メモリーをレジスタとする計算モデル

純粋なスタックマシンはスタックだけで構成しますが、通常はランダムアクセスができるメモリーと組み合わせで使われることが多いです。これはレジスタマシンでも同様で、通常はレジスタ以外にメモリーを使用します。

スタックマシンにはJVMの他にも.NET Frameworkでも使用されています。

一方、既存のほとんどのCPUがレジスターマシンになります。

たとえば、レジスターマシンで加算を行う場合、aレジスタの値とbレジスタの値を加算して結果をaレジスタに格納するというような命令になります(add a, bのような感じです)。

命令の対象が明確に記述されているので、分かりやすいはずです(とはいうものの、機械語でプログラムを書けと言われてもイヤですけど😰)

では、スタックマシンはどのように動作するのでしょう?

ここでは、単純な例としてHPの電卓でスタックマシンの動作を説明していきます。

 

HPの電卓

HPというと、紆余曲折あってPCやプリンターのHP Inc.とサーバーのHPEになっていますが、かつては計測機器を扱う会社でした。そんなHPが1970年代から2000年代にかけて電卓を作っていたのでした。

当時は、関数電卓やプログラム電卓といったらHPという感じで、そこそこ使われていました。プログラム電卓というのは、その名の通りプログラムが組める電卓です。

このHPの電卓は、なんといっても入力方式が独特でした。

たとえば、1+2を計算する場合、通常の電卓であれば 1 + 2 = と入力しますね。これに対し、HPの電卓は 1 [Enter] 2 [Enter] + と入力しました([Enter]というボタンがあったのです)。

[Enter]を省略して記述すると 1 + 2 は 1 2 + になるということです。この数式の書き方は逆ポーランド記法と呼ばれています。

 

逆ポーランド記法

ちょっと脱線気味ですが、逆ポーランド記法についても説明しておきましょう。

「逆」とついていることから分かるかもしれませんが、「逆」ではないポーランド記法もあります。というか、こちらが先ですね。

ポーランド記法はJan Łukasiewicz (ヤン・ウカシェヴィチ)が発案した数式の記法です。

演算子を先に記述することから前置記法とも呼ばれます。

私たちが通常使用している 1 + 1 のような記法は演算子が数値の間に記述されるので中置記法と呼びます。逆ポーランド記法は演算子が最後なので、後置記法になります。

 

逆ポーランド記法の利点はカッコを使用せずに数式を記述できるところです。

たとえば、以下の数式はどうでしょう。

(2 + 3) × 5 + (4 - 2) ÷ 2

これを逆ポーランド記法で記述すると以下のようになります。

2 3 + 5 × 4 2 - 2 ÷ +

カッコがないというのは、電卓でメモリ機能(M+やM-、MRCなど)を使わなくても計算ができるということで、入力が簡単になります。まぁ、逆ポーランド記法で考えなければいけないというハードルはありますけど。

そして、もう1つの利点が、逆ポーランド記法はスタックを使えば簡単に実装できるということです。

 

スタックを使用した逆ポーランド記法の計算

では、逆ポーランド記法の数式をスタックを使用して計算してみましょう。

ルールは簡単です。

  1. 数値であれば(HPの電卓では、[Enter]が入力されたら)その値をスタックに積む
  2. 演算子であれば、演算に必要な個数のデータをスタックから取り出し、計算結果を再びスタックに積む

では、1 + 2をやってみましょう。

 

電卓であれば、スタックの先頭を表示していれば計算結果が表示されます。

複雑な数式でも計算の途中結果がスタックに保持されているので、スタックとは別のメモリーを使用しなくても計算が実行できます。

 

電卓では計算だけですが、一般のスタックマシンでも同様に必要なデータをスタックに置き、処理の結果を再びスタックに置くという過程で処理が進みます。

これはJVMでも同様です。

では、次回は実際にJVMでどのようにスタックマシンが構成されているのか紹介する予定です。

2025/02/10

バイトコード入門 その1 準備編

先月のJJUGナイトセミナーで、Class-File APIとその前提知識となるバイトコードについてプレゼンしてきました。資料はこちら。

 

おかげさまで、コロナ後のオフラインに戻してから初のキャンセル待ちとなるぐらい盛況でした。もっともバイトコードの話よりも、増田さんのアーキテクチャーの話の方が期待されていたとは思いますけど。

 

まぁ、それはそれとして、バイトコードがどのように実行されるかというところから、Class-File APIまでを50分で説明するのは分量的になかなか難しく、特にClass-File APIの方はかなりはしょった説明になってしまいました。

そういえば、今までJava in the Boxでもバイトコードに関して触れたことがなかったので、いい機会ですし、バイトコードって何というところから説明していきたいと思います。

  1. 準備編 (今回)
  2. スタックマシン
  3. バイトコード処理の構成

 

バイトコードって何?

Javaでシステムを作っているだけであれば、バイトコードに触れる機会はまずないはずです。

バイトコードというのはJava特有の言葉ということではなく、他にも使われる言葉です。

一般的には仮想マシンなどの実行環境が解釈するための中間表現です。人間が読むことを想定しておらず、バイナリーで記述されるため、バイトコードと呼ばれるようです。

Javaの場合、バイトコードは当然ながらJVMが解釈するために使用されます。

バイトコードは約200種類のJVMに対する命令(オペコードもしくはインストラクションと呼ばれます)からなっています。もちろん、バイナリーなので16進数表記で表されますが、さすがにそれでは読みにくいのでloadやstoreなど命令に1対1に対応づいた表記で表されることが多いです。

機械語に対しうるアセンブラのようなものですね。

具体的な命令などは次回以降に説明します。

 

バイトコードはどこに記述される?

Javaのソースコードをjavacでコンパイルすると、クラスファイルが生成されます。このクラスファイルにバイトコードが含まれています。

なお、クラスファイルにはバイトコード以外にも実行に必要な情報が含まれています。

バイトコードの定義や、クラスファイルのフォーマット定義などはJava Virtual Machine Specification (JVS)に記載されています。

Java言語の仕様であるJava Language Specification (JLS)は、Javaのバージョンごとに改定されていますが、JVMSも同じくバージョンごとに改定されます。

最新版のJava 23でのJVMSは以下のURLで参照できます。

クラスファイルのフォーマットは4章、バイトコードは6章に記載されています。

クラスファイルには大まかにいうと、以下の4つの情報が記録されています。

  1. クラスファイルの情報
  2. クラスの情報
  3. コンスタントプール
  4. アトリビュート

クラスファイルの情報にはクラスファイルのバージョンなどが含まれます。Java言語にもバージョンがありますが、クラスファイルにもバージョンがあるのです。

たとえば、Java 23のjavacでコンパイルされたクラスファイルのバージョンは67になります。

クラスの情報は、クラス名やスーパークラス、実装しているインタフェースなどです。コンスタントプールはクラスで使用される様々な定数を定義していあります。

最後のアトリビュートは様々な情報を記載することができ、バイトコードもそのうちの1つになります。

アトリビュートにはバイトコード以外にフィールド定義やメソッド定義などが記載されています。

 

javapコマンド

前述したようにバイトコードはバイナリで表されます。同様にクラスファイルも人が読むことを想定していないため、バイナリファイルです。

とはいうものの、クラスファイルに何が記述されているのか確認したいこともありますよね。

こういう時に使用するのが、JDKに含まれているjavapコマンドです。

javapコマンドはいろいろな情報が出せるので、さっそく試してみましょう。

サンプルに使うのはおなじみのHello, World!です。

public class Hello {
    static final String HELLO = "Hello, World!";

    private void sayHello() {
        System.out.println(HELLO);
    }
    
    public static void main(String... args) {
        new Hello().sayHello();
    }
}

 

まず、javacでコンパイルしてから、javapを実行します。javapの引数はクラス名もしくはクラスファイル名です。

> javac Hello.java

> javap Hello
Compiled from "Hello.java"
public class Hello {
  static final java.lang.String HELLO;
  public Hello();
  public static void main(java.lang.String...);
}

>

 

何もオプションを指定せずにjavapを実行すると、クラスの情報と宣言されているフィールド、メソッドの情報が出力されます。

しかし、何か抜けているような気がしませんか。

そう、プライベートメソッドのsayHelloメソッドが抜けているのです。

javapはデフォルトでは、パッケージプライベートで宣言されたフィールド、メソッドしか出力しません。プライベートで宣言されたフィールド、メソッドを出力するには、オプションの-privateもしくは-pを指定します。

> javap -p Hello
Compiled from "Hello.java"
public class Hello {
  static final java.lang.String HELLO;
  public Hello();
  private void sayHello();    
  public static void main(java.lang.String...);
}

>

 

この出力結果を見ると、Helloクラスはパブリッククラスで、フィールドはパッケージプライベートでstatic finalのHELLO、メソッドはデフォルトコンストラクター、sayHelloメソッド、そしてstaticメソッドのmainが宣言されていることが分かります。

元のHello.javaにはデフォルトコンストラクターは記述されていませんが、コンパイル時に自動生成されます。

 

バイトコードの解析: -cオプション

javapコマンドでバイトコードを出力するには、-cオプションを使用します。

> javap -p -c Hello
Compiled from "Hello.java"
public class Hello {
  static final java.lang.String HELLO;

  public Hello();
    Code:
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return

  private void sayHello();
    Code:
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #15                 // String Hello, World!
         5: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return

  public static void main(java.lang.String...);
    Code:
         0: new           #13                 // class Hello
         3: dup
         4: invokespecial #23                 // Method "<init>":()V
         7: invokevirtual #24                 // Method sayHello:()V
        10: return
}

>

 

この場合でも、-pオプションを使用しないとプライベートフィールド、メソッドは出力されないので、忘れないようにしましょう。

バイトコードはメソッド定義の後のCode:の次の行から始まります。aload_0やinvokespecialなどがバイトコードの命令です。

invokespecial #1のように、#と数字で表示されているのはコンスタントプールを指しています。#1はコンスタントプールのインデックス1の定数です。具体的にはコメントで示されているように、Objectクラスのデフォルトコンストラクタへのメソッド参照です。

なお、このコメントはjavapが解析結果を付記したもので、クラスファイルに記載されているわけではありません。

このコメントがあると、バイトコードやコンスタントプールを読む手間がかなり省けるはずです。

 

全部出力: -verbose/-vオプション

バイトコードだけでなく、コンスタントプールの値や、その他のアトリビュート、フラグなどの情報を出力するには-verboseもしくは-vオプションを使用します。

ちょっと長いですが、Helloクラスを-vでの出力を以下に示します。

 

> javap -p -v Hello
Classfile /temp/Hello.class
  Last modified 2025/02/09; size 557 bytes
  SHA-256 checksum 5b4893687f4d64e1e6a659d495f8febf0680a2a3d8a036c2e40c5057d2d0c5d1
  Compiled from "Hello.java"
public class Hello
  minor version: 0
  major version: 68
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #13                         // Hello
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // java/lang/System.out:Ljava/io/PrintStream;
   #8 = Class              #10            // java/lang/System
   #9 = NameAndType        #11:#12        // out:Ljava/io/PrintStream;
  #10 = Utf8               java/lang/System
  #11 = Utf8               out
  #12 = Utf8               Ljava/io/PrintStream;
  #13 = Class              #14            // Hello
  #14 = Utf8               Hello
  #15 = String             #16            // Hello, World!
  #16 = Utf8               Hello, World!
  #17 = Methodref          #18.#19        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #18 = Class              #20            // java/io/PrintStream
  #19 = NameAndType        #21:#22        // println:(Ljava/lang/String;)V
  #20 = Utf8               java/io/PrintStream
  #21 = Utf8               println
  #22 = Utf8               (Ljava/lang/String;)V
  #23 = Methodref          #13.#3         // Hello."<init>":()V
  #24 = Methodref          #13.#25        // Hello.sayHello:()V
  #25 = NameAndType        #26:#6         // sayHello:()V
  #26 = Utf8               sayHello
  #27 = Utf8               HELLO
  #28 = Utf8               Ljava/lang/String;
  #29 = Utf8               ConstantValue
  #30 = Utf8               Code
  #31 = Utf8               LineNumberTable
  #32 = Utf8               main
  #33 = Utf8               ([Ljava/lang/String;)V
  #34 = Utf8               SourceFile
  #35 = Utf8               Hello.java
{
  static final java.lang.String HELLO;
    descriptor: Ljava/lang/String;
    flags: (0x0018) ACC_STATIC, ACC_FINAL
    ConstantValue: String Hello, World!

  public Hello();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  private void sayHello();
    descriptor: ()V
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #15                 // String Hello, World!
         5: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8

  public static void main(java.lang.String...);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0089) ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=2, locals=1, args_size=1
         0: new           #13                 // class Hello
         3: dup
         4: invokespecial #23                 // Method "<init>":()V
         7: invokevirtual #24                 // Method sayHello:()V
        10: return
      LineNumberTable:
        line 9: 0
        line 10: 10
}
SourceFile: "Hello.java"

>

 

Constant pool:で始まる次の行からがコンスタントプールです。

先ほど-cオプションで出力したバイトコードで#1が参照されていましたが、このコンスタントプールの表で見てみましょう。

#1の行にはメソッドの参照を示すMethodrefに続いて#2.#3と記載されています。

#2のClassはクラスの参照を示しており、その名前は#4に記載されいます。

#4の後のUtf8は文字列を表しています。#4で定義されている文字列定数はjava/lang/Object、つまり#2で参照していたクラス名です。

では、#1で参照しているもう1つの#3を見てみましょう。

#3はNameAndType、つまりメソッド名と型(ここではクラス名ではなく、メソッドのシグネチャーです)を示しており、#5と#6を参照しています。

#5は文字列定数で<init>、これはコンストラクタを表しています。#6も文字列定数で()Vです。これはメソッドのシグネチャーが引数なし、戻り値がvoidであることを示しています。

このように、コンスタントプールは参照、参照となっていますが、各々の行にコメントが追記されているので、これを見れば参照を追わずとも分かるはずです。

 

フィールドやクラスも追加の情報が記載されていることが分かると思いますが、これらに関してはバイトコードの詳細説明の時に触れる予定です。

 

準備編まとめ

  • バイトコードはJVMが解釈するための中間表記
  • クラスファイルにはバイトコード以外に実行に必要な情報が記載される
  • クラスファイルの解析コマンド javap
  • javapでプライベートを含めて出力: -pオプション
  • javapでバイトコードを出力: -cオプション
  • javapで全部出力: -vオプション

 

次回は、バイトコードの詳説に移りたいところですが、その前の事前知識としてスタックマシンについて紹介する予定です。

2025/01/09

Stream Gatherer 動作編

年が変わってしまいましたが、前回の続き。前回はこちら

全開でGathererの使い方を一通り説明したので、今回はGathererがどのように動作しているのかを解説していきます。

 

Stream APIの動作

Gathererがどのように動作するのかを説明する前に、もともとのStream APIがどのように動作しているのか復習しておきましょう。

といっても、詳しく説明すると長くなってしまうので、簡単に。

ここではIntStreamインタフェースなどのプリミティブ系は省略、またシーケンシャルな動作に限定して説明します。

Streamの実行に重要なインタフェース

Stream APIの動作を説明する前に、Stream APIを使っているだけであれば出てこない、でも実装では重要なインタフェースを2つだけ先に紹介しておきます。

  • java.util.Spliterator
  • java.util.stream.Sink

Spliteratorインタフェースは公開インタフェースで、Sinkインタフェースはパッケージプライベートなインタフェースです。

1つ目のSpliteratorインタフェースはSplit + Iteratorのことで、簡単にいえばストリームのイテレーションを制御するインタフェースになります。

Splitがパラレルの場合で、分割統治法により個々の要素を処理します。Iteratorがシーケンシャルの場合で、こちらは普通のイテレーターですね。

Spliteratorオブジェクトはソースによって実装クラスが異なり、ソースからStreamオブジェクトを生成する時に一緒に作られます。

もう一方のSinkは、台所にあるシンクと同じ単語です。動詞だと「沈む」もしくは「沈める」という意味です。

何が沈んでいるかというと操作です。中間操作や終端操作で行われる操作がSinkに沈められています。

SinkインタフェースはConsumerインタフェースのサブインタフェースで、acceptメソッドがコールされると、その操作が実行されます。

 

Sinkオブジェクトで操作を実行する仕組み

では、コードを使って説明していきましょう。ここでは、次のコードで説明していきます。

    var stream = Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
    var stream2 = stream.map(i -> Integer.toString(i));
    var result = stream2.reduce("", (prev, pres) -> prev + pres);

 

Integerのリストを文字列化して、文字列の連結を行うコードです。

さて、ここで変数stream2の型はどうなるでしょう?

もちろん、Streamインタフェースではありますが、Streamインタフェースを実装したコンクリートクラスの方です。

答えはStatelessOpクラスの匿名クラスです。

このStatelessOpクラスが、mapメソッドやfilterメソッドなど状態を持たない中間操作で使用されるクラスです。

StatelessOpクラスのスーパークラスがReferencePipelineクラスで、ReferencePipelineクラスがStreamインタフェースを実装しています。

Referenceなのはストリームを流れる要素が参照型だからで、intであればIntPipelineクラスになります。

それはそれとして、ここで使われているStatelessOpクラスの匿名クラスを生成している部分が次のコードになります。ここでは、mapメソッドの場合です。

    public final <R> Stream<R> map(Function<? super P_OUT, ? extends R> mapper) {
        Objects.requireNonNull(mapper);
        return new StatelessOp<>(this, StreamShape.REFERENCE,
                StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
            @Override
            Sink<P_OUT> opWrapSink(int flags, Sink<R> sink) {
                return new Sink.ChainedReference<>(sink) {
                    @Override
                    public void accept(P_OUT u) {
                        downstream.accept(mapper.apply(u));
                    }
                };
            }
        };
    }

 

StatelessOpクラスの匿名クラスを作る時に、opWrapSinkメソッドをオーバーライドしています。ここで、Sinkが出てきましたね。

opWrapSinkメソッドの中ではSink.ChainedReferenceクラスの匿名クラス生成しています。もちろん、Sink.ChainedReferenceクラスはSinkインタフェースの実装クラスです。

Sink.ChainedReferenceクラスはその名の通りSinkをチェーンでつなげていくインタフェースです。フィールドに下流のSinkオブジェクトであるdownstreamを保持しています。

Gathererでもdownstreamが出てきましたが、ここでは次段のSinkオブジェクトを表しています。

そして、赤字で示したacceptメソッドで、mapメソッドの引数で指定された関数(Functionインタフェースのラムダ式)を実行し、その結果を引数にして下流のSinkオブジェクトのacceptメソッドをコールしています。

これで、中間操作の操作を順々に実行する仕組みができました。

 

終端操作のSinkオブジェクトを生成する仕組み

実際に中間操作から終端操作まで操作をつなげていくのは、終端操作の時です。

まずは終端操作に対応するSinkオブジェクトを作るしくみです。これはreduceメソッドの内部でコールされるReduceOps.makeRefメソッドで行われます。

    public final <R> R reduce(R identity, BiFunction<R, ? super P_OUT, R> accumulator, BinaryOperator<R> combiner) {
        return evaluate(ReduceOps.makeRef(identity, accumulator, combiner));
    }

 

ReduceOpsクラスは、ReduceOpクラスのユーティリティクラスですね。中間操作で使用したStatelessOpの終端操作版がReduceOpクラスになります。

おもしろいことに、ReduceOpクラスはReduceOpsクラスのインナークラスになっています。

さて、そのmakeRefメソッドは以下のようになっています。

    public static <T, U> TerminalOp<T, U>
    makeRef(U seed, BiFunction<U, ? super T, U> reducer, BinaryOperator<U> combiner) {
        Objects.requireNonNull(reducer);
        Objects.requireNonNull(combiner);
    
        class ReducingSink extends Box<U> implements AccumulatingSink<T, U, ReducingSink> {
            @Override
            public void begin(long size) {
                state = seed;
            }

            @Override
            public void accept(T t) {
                state = reducer.apply(state, t);
            }

            @Override
            public void combine(ReducingSink other) {
                state = combiner.apply(state, other.state);
            }
        }
    
        return new ReduceOp<T, U, ReducingSink>(StreamShape.REFERENCE) {
            @Override
            public ReducingSink makeSink() {
                return new ReducingSink();
            }
        };
    }

 

makeRefメソッドの内部で、青字で示したReducingSinkクラスを定義しています。ここでSinkが出てきました。

このReducingSinkクラスが終端操作に対応するSinkです。

ただし、ReducingSinkクラスは中間操作で使用したSinkとはちょっと異なります。

それはReducingSinkクラスがBoxクラスのサブクラスだということです。Boxクラスは値を1つだけ保持するコンテナクラスです。

Gathererと同じで、終端操作は状態を持ち、最終的に処理の結果を返します。その状態を保持するためにBoxクラスを使用しています。

そして、makeRefメソッドの戻り値としてReduceOpクラスの匿名クラスを生成し、その内部でmakeSinkメソッドをオーバーライドし、ここで定義したReducingSinkオブジェクトを返すようにしています。

makeSinkメソッドは定義しましたが、ここでコールされるわけではありません。したがって、まだSinkオブジェクトは生成されていません。

 

中間操作から終端操作までのSinkオブジェクトを連ねる

ここまでで、中間操作と終端操作のSinkオブジェクトを生成する仕組みを見てきました。後は、中間操作から終端操作にいたるSinkオブジェクトを生成し、一連の処理をつなげる必要があります。

これは、reduceメソッドの内部でコールされているevaluateメソッドで行われます。

evaluateメソッド内では処理がシーケンシャルかパラレルかによって処理が分かれますが、ここではシーケンシャルに処理するevaluateSequentialメソッドを見ていきます。

次のコードはReduceOpクラスのevaluateSequentialメソッドです。

    public <P_IN> R evaluateSequential(PipelineHelper<T> helper,
                                       Spliterator<P_IN> spliterator) {
        return helper.wrapAndCopyInto(makeSink(), spliterator).get();
    }

 

ここで、赤字で示したmakeSinkメソッドが出てきました。

makeRefメソッドの中で、ReduceOpクラスの匿名クラスを定義し、makeSinkをオーバーライドしていましたが、そのmakeSinkメソッドをコールするのがevaluateSequentialメソッドの中でした。

これで、終端操作に対応するSinkオブジェクトが生成できました。

一方のSpliteratorオブジェクトも出てきました(変数spliterator)。Spliteretorオブジェクトの生成は、ソースからStreamオブジェクトを作成する時に作られます。ここでは、省略しますが、もし興味があれば、ソースからStreamオブジェクトを生成する部分を見てみるのもおもしろいと思います。

さて、evaluateSequentialメソッドの第1引数のhelperは、実はReduceOpオブジェクト自身です。

継承関係をさかのぼっていくと、ReferencePipelineクラスのスーパークラスがAbstractPipelineクラスで、さらにそのスーパークラスがPipelineHelperクラスになります。

evaluateSequentialメソッドの内部でコールされているwrapAndCopyIntoメソッドがPipelineHelperクラスで定義されているためこうなっているとは思いますが、ちょっと分かりにくいですね。

PipelineHelperクラスのwrapAndCopyIntoメソッドはabstractとして定義されており、AbstractPipelineクラスでオーバーライドされています。

    final <P_IN, S extends Sink<E_OUT>> S wrapAndCopyInto(S sink, Spliterator<P_IN> spliterator) {
        copyInto(wrapSink(Objects.requireNonNull(sink)), spliterator);
        return sink;
    }

 

赤字で示したwrapSinkメソッドがSinkオブジェクトを連ねる処理を行いそうなことが分かります。

中間操作のmapメソッドの中でStatelessOpeクラスの匿名クラスがopWrapSinkメソッドをオーバーライドしていたのを思い出してください。

では、そのwrapSinkメソッドです。

    final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
        Objects.requireNonNull(sink);

        for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
            sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
        }
        return (Sink<P_IN>) sink;
    }

 

for文で自分自身(終端操作のReduceOpオブジェクト)からパイプラインをさかのぼって、opWrapSinkメソッドをコールしています。

opWrapSinkメソッドの内部ではSinkオブジェクトを生成しているので、これで終端操作から中間操作の先頭までのSinkオブジェクトを生成して、チェーンでつなげていくことができました。

残るは、ここで生成したSinkオブジェクトに対して、登録されている処理を行う部分です。

 

Spliteratorを使用したイテレーション

やっと最後のイテレーションの部分にまで到達しました。

先ほどのwrapAndCopyIntoメソッドでwrapSinkメソッドの戻り値(Sinkオブジェクト)とSpliteratorオブジェクトを引数にしてコールされるのが、copyIntoメソッドです。

copyIntoメソッドもAbstractPipelineクラスでオーバーライドされています。

    final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
        Objects.requireNonNull(wrappedSink);

        if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
            wrappedSink.begin(spliterator.getExactSizeIfKnown());
            spliterator.forEachRemaining(wrappedSink);
            wrappedSink.end();
        }
        else {
            copyIntoWithCancel(wrappedSink, spliterator);
        }
    }

 

if文でShort Circuitの有無で処理を変えています。

前回、Short Circuitが出てきましたが、イテレーションの途中で停止させるのがShort Circuitです。

Short Circuitの可能性がある場合、イテレーションを継続するか停止するかチェックする必要があるため別メソッド(copyIntoWithCancelメソッド)になっています。

イテレーションの本体は赤字で示したforEachRemainingメソッドです。

Gatherer.IntegratorインタフェースのファクトリーメソッドであるofGreedyメソッドを使った場合も、Short CircuitされることはないのでforEachRemainingメソッドが使用されます。

 

forEachRemainingメソッドを定義しているSpliteratorインタフェースの実装クラスはソースによって異なります。ArrayListクラスや配列の場合、ArraySpliteratorクラスが使われます。

サンプルのコードはStream.ofメソッドでStreamオブジェクトを生成していますが、この場合もArraySpliteratorクラスが使われます。

ArraySpliteratorクラスのforEachRemainingメソッドを次に示します。

    public void forEachRemaining(Consumer<? super T> action) {
        Object[] a; int i, hi; // hoist accesses and checks from loop
        if (action == null)
            throw new NullPointerException();

        if ((a = array).length >= (hi = fence) &&
            (i = index) >= 0 && i > (index = hi)) {
            do { action.accept((T)a[i]); } while (++i < hi);
        }
    }

 

forEachRemainingメソッドの引数の型がConsumerインタフェースになっていますが、SinkインタフェースはConsumerインタフェースのサブインタフェースなので、実態はSinkインタフェースです。

メソッド内では、配列の範囲チェックの後に、do-while文でループします。

このループで、配列の要素を引数にして変数actionのacceptメソッドをコールしています。

この変数actionは最初の中間操作を保持しているSinkオブジェクトなので、中間操作から終端操作まで順々に実行されます。

 

簡単に紹介するつもりでしたが、かなり長くなってしまいました。

 

Stream Gathererの動作

やっとGathererです。

ここまでのStream APIの動作が理解できていれば、Gathererを理解するのも簡単です。

GathererのPipelineとSink

mapやfilterなど状態を持たない中間操作のパイプラインにはStatelessOpクラスが使われてきました。また、SinkにはSink.ChainedReferenceクラスが使われています。

これに対し、Gathererでは専用のパイプラインクラスであるGathererOpクラスが使用されます。また、SinkもGatherSinkクラスが使われます。

これをコードで確かめてみましょう。以下のコードはReferencePipelineクラスのgatherメソッドです。

    public final <R> Stream<R> gather(Gatherer<? super P_OUT, ?, R> gatherer) {
        return GathererOp.of(this, gatherer);
    }

 

ofメソッドがファクトリーメソッドになっており、GathererOpオブジェクトを返しています。

GathererOpクラスのopメソッドはパイプラインの前段がGathererOpクラスであれば、合成する処理が含まれていますが、基本的にはGathererOpクラスのオブジェクトを生成しているだけです。

もう一方のSinkの方ですが、mapメソッドなどではStatelessOpクラスのopWrapSinkメソッドをオーバーライドしていたのを思い出してください。

GathererOpクラスはGatherer専用のクラスなので、opWrapSinkメソッドもオーバーライドされずに、そのまま使われます。

以下にGathererOpクラスのopWrapSinkメソッドを示します。

    Sink<T> opWrapSink(int flags, Sink<R> downstream) {
        return new GatherSink<>(gatherer, downstream);
    }

 

ここでGatherSinkクラスが出てきました。

GatherSinkクラスもGatherer専用なので、匿名クラスなどを使わずに、そのまま使われます。

では、GatherSinkクラスの定義とコンストラクターを見ておきましょう。

    static final class GatherSink<T, A, R> implements Sink<T>, Gatherer.Downstream<R> {
        private final Sink<R> sink;
        private final Gatherer<T, A, R> gatherer;
        private final Integrator<A, T, R> integrator; // Optimization: reuse
        private A state;
        private boolean proceed = true;
        private boolean downstreamProceed = true;

        GatherSink(Gatherer<T, A, R> gatherer, Sink<R> sink) {
            this.gatherer = gatherer;
            this.sink = sink;
            this.integrator = gatherer.integrator();
        }

 

GatherSinkクラスはSinkインタフェースを実装しているのは当然ですが、Gatherer.Downstreamインタフェースも実装しています。

Gatherer.Downstreamインタフェースは、Gatherer.Integratorインタフェースのintegratメソッドで使用するpushメソッドを定義しています。

前回は下流に対してデータを流すという説明をしていましたが、実際はGatherSinkクラスのpushメソッドがコールされるわけです。

また、Gatherer.Downstreamインタフェースを実装しているため、ごっちゃにならないように下流のSinkを表すフィールドはdownstreamではなくsinkという名前になっています。

 

GatherSinkクラスの動作

前述したように、終端操作のパイプラインにおいて、forEachRemainingメソッドでイテレーションが実行されます。その時に、Sinkオブジェクトのacceptメソッドがコールされます。

これはGathererを使った場合も同じです。GatherSinkオブジェクトのacceptメソッドがコールされます。

では、GatherSinkクラスのacceptメソッドを見てみましょう。

        public void accept(T t) {
            /* Benchmarks have indicated that doing an unconditional write to
             * `proceed` is more efficient than branching.
             * We use `&=` here to prevent flips from `false` -> `true`.
             *
             * As of writing this, taking `greedy` or `stateless` into
             * consideration at this point doesn't yield any performance gains.
             */
            proceed &= integrator.integrate(state, t, this);
        }

 

コメントがおもしろいですけど、主題とは関係ないので...

acceptメソッドの内部では、Gatherer.Integratorインタフェースのintegrateメソッドがコールされています。

通常の中間操作であれば、下流のSinkオブジェクトのacceptメソッドをコールするのですが、ここではそれがありません。

Gathererを使う時は、下流にデータを流すかどうかはGathererによるためです。

とはいえ、下流にデータを流す(つまり、下流のSinkオブジェクトのacceptメソッドをコールすることに相当します)場合もあります。これはどこで行っているのでしょうか。

ヒントはintegrateメソッドの第3引数です。

前回、integratorメソッドの説明で、下流にデータを流す時はGatherer.Downstreamインタフェースのpushメソッドをコールしますと説明しました。

そのpushメソッドはintegrateメソッドの第3引数のdownstream変数に対して行っていたことを覚えていますでしょうか。

では、第3引数が何かというと、上のコードではthisを渡しています。

GatherSinkクラスはGatherer.Downstreamインタフェースを実装していますと前述しました。ということは、結局、自分自身のpushメソッドをコールしているということになります。

では、そのpushメソッドを見てみましょう。

    public boolean push(R r) {
        var p = downstreamProceed;
        if (p)
            sink.accept(r);
        return !cancellationRequested(p);
    }

 

ここで、下流のSinkオブジェクトに対してacceptメソッドをコールしていました。

つまり、Gathererで下流にデータをpushした時に、次段から処理が行われるということです。

このようにして、二重ループではなく、効率的に状態を保持した中間操作を行うことができるのです。

 

まとめ

いちおうまとめておきましょう。

Stream APIの動作は要約すると次のようになります。

  1. Spliteratorがイテレーションを行う
  2. Sinkが処理をまとめて、一括してデータを処理する

これに対し、GathererではSinkを工夫することで、下流にデータを流した時にだけ次段から連なる処理を行うようにします。

 

今回はストリームの開始時と終了時の処理や、パラレル処理の場合を省略しましたが、もし興味があるのであればソースを見てみるとおもしろいですよ。