Slide 1

Slide 1 text

Head toward Java 22 (and Java 23) KUBOTA Yuji LINEヤフー株式会社 Japan Java User Group Night Seminar 2024/03/27

Slide 2

Slide 2 text

KUBOTA Yuji (@sugarlife) Manager at LINEヤフー株式会社, IMF team IMF team = Kafka infrastructure team エンジニアリングの究極的な挑戦の場はここにある Kafkaイン フラを全社に提供するIMFチームの仕事のやりがい: https://logmi.jp/tech/articles/325945 「膨大な量のトラフィックを扱えるLINEの環境は魅力的」 Kafkaスペシャリストが語る仕事の醍醐味: https://engineering.linecorp.com/en/interview/kafka-okada 2

Slide 3

Slide 3 text

Recent updates JDK JEP Non-Standard JEP Remark 17 14 3 LTS 18 9 3 - 19 7 6 - 20 7 7 - 21 15 7 LTS 22 12 8 - 23 1 1 In development 1: Incubator or Preview or Experimental 3

Slide 4

Slide 4 text

4

Slide 5

Slide 5 text

JEPで導入される機能のフェーズ Incubator (JEP 11): Java APIの試験用モジュール( jdk.incubator )。標準化に向けてフィ ードバックを得て変更しやすいように特別なモジュールにしている。有効にするには --add- modules jdk.incubator.xxx の指定が必要 Preview (JEP 12): Java言語の試験機能。有効にするには --enable-preview の指定が必 要。コンパイル時には --source XX か --release XX の指定も必要( XX はバージョン) Experimental(試験機能): GCやコンパイラなどのランタイムの試験機能。有効にするには機 能ごとのオプション以外に、 -XX:+UnlockExperimentalVMOptions の指定が必要 Standard(標準機能): 上記の試験フェーズを通じて標準機能に昇格した機能。リリースサイク ルは6ヶ月だがフィードバック対応はおおよそ3ヶ月しかない(残り3ヶ月は安定化対応)ので、 おおよそ2回以上のバージョンアップを挟んで昇格することが多い 5

Slide 6

Slide 6 text

JEPs in JDK 22 JEP 423: Region Pinning for G1 JEP 447: Statements before super(...) (Preview) JEP 454: Foreign Function & Memory API JEP 456: Unnamed Variables & Patterns JEP 457: Class-File API (Preview) JEP 458: Launch Multi-File Source-Code Programs JEP 459: String Templates (Second Preview) JEP 460: Vector API (Seventh Incubator) JEP 461: Stream Gatherers (Preview) JEP 462: Structured Concurrency (Second Preview) JEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview) JEP 464: Scoped Values (Second Preview) 6

Slide 7

Slide 7 text

JEP in JDK 23 JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview) 7

Slide 8

Slide 8 text

JEP 423: Region Pinning for G1 JNIによってJava threadがcritical regionにいる間、GCが無効化される (GCLocker) GetXXXCritical --(GC停止)--> ReleaseXXXCritical (doc) GCLocker Initiated GC が起動する critical objectがいるリージョン(Region)をPinningする(Evacuationさせない)ことでGC を待たせることなく処理できるようになった ここでのリージョン = G1 GCにおけるヒープ管理の単位 (slide) Pinning自体はすでにHumongous ObjectやClass Data Sharingなどで導入済み OOME発生可能性の緩和とパフォーマンス改善(全体のstall回避)の効果がある Shenandoahは(おそらく当初から)対応済み。ZGCは"Open"状態 JDK-8269423 1: GCCauseとしてGCログに出力される: https://github.com/openjdk/jdk/blob/jdk- 23+15/src/hotspot/share/gc/shared/gcLocker.cpp#L165 8

Slide 9

Slide 9 text

JEP 447: Statements before super(...) (Preview) 親クラスのコンストラクタ(以下 super() )呼び出し前に処理が書けるようになった 前提 初期化前のフィールドのアクセスは禁じる必要がある 子クラスのフィールド初期値は親クラスのフィールドの初期値に依存することがあ るのでコンストラクタはトップダウンで呼び出される必要がある このため、子クラスのコンストラクタは super() が完了するまで、親クラスおよ び自クラスのフィールドにアクセスしてはならない トップダウンでの実行を保証するために、コンストラクタでは他コンストラクタの 明示的な呼び出しを最初にする制約がある この明示的な呼び出しを行う際に引数のどれもが現在のオブジェクト( this )にア クセスできないようにする制約がある 9

Slide 10

Slide 10 text

super(...) の引数に対する処理を書きたい 例:値の検証 Before // Pattern 1: 素直に書く public class Child extends Parent { public Child(String name) { super(name); // 制約による親クラスのコンストラクタ呼び出し if (name.equals("Anakin")) throw new IllegalArgumentException("you're not my father"); } } 10

Slide 11

Slide 11 text

super(...) の引数に対する処理を書きたい 例:値の検証 Before // Pattern 2: コンストラクタ呼び出し前に処理を実行させる public class Child extends Parent { public Child(String name) { super(verifyFather(name)); } // static で書く必要がある検証メソッド private static int verifyFather(String name) { if (name.equals("Anakin")) throw new IllegalArgumentException("you're not my father"); return name; } } 11

Slide 12

Slide 12 text

super(...) の引数に対する処理を書きたい 例:値の検証 After public class Child extends Parent { public Child(String name) { if (name.equals("Anakin")) throw new IllegalArgumentException("you're not my father"); super(name); } } 12

Slide 13

Slide 13 text

this(...) の引数に対する処理を書きたい 例:値の準備 After public record Name(String first, String last) { public Name(String fullName) { var names = fullName.split(" "); // 今まではこれがNG this(names[0], names[1]); } } var a = new Name("Yuji KUBOTA"); a ==> Name[first=Yuji, last=KUBOTA] 13

Slide 14

Slide 14 text

super(...) の引数を共有したい Before public class Super { public Super(F f1, F f2) { ... } } public class Sub extends Super { private Sub(F f) { super(f, f); // 最初に呼ぶ必要があるのでこのコンストラクタを作る必要がある // 今回は `super(new F(), new F());` でも良いが2つ目は本来不要 } public Sub() { this(new F()); } } 14

Slide 15

Slide 15 text

super(...) の引数を共有したい After public Sub() { var f = new F(); super(f, f); } 15

Slide 16

Slide 16 text

NG case - コンストラクト前のインスタンスはアクセス不可 class A { int i; A() { this.i++; // Error this.hashCode(); // Error System.out.print(this); // Error i++; // Error hashCode(); // Error super(); } } class B extends A { B() { super.i++; // Error super(); } } 16

Slide 17

Slide 17 text

NG case - コンストラクト前のインスタンスはアクセス不可 class Outer { int outer; void hello() { System.out.println("Hello"); } Outer() { new Inner(); // Error - 'this' is enclosing instance super(); } class Inner { int inner; Inner() { Outer.this.outer++; // Allowed - enclosing instance Inner.this.inner++; // Error - same instance hello(); // Allowed - enclosing instance method super(); } } } 17

Slide 18

Slide 18 text

$ javac --enable-preview --source 23 ConstructionTest.java ConstructionTest.java:7: error: cannot reference this before supertype constructor has been called this.i++; // Error ^ ConstructionTest.java:8: error: cannot reference this before supertype constructor has been called this.hashCode(); // Error ^ ConstructionTest.java:9: error: cannot reference this before supertype constructor has been called System.out.print(this); // Error ^ ConstructionTest.java:10: error: cannot reference i before supertype constructor has been called i++; // Error ^ ConstructionTest.java:11: error: cannot reference hashCode() before supertype constructor has been called hashCode(); // Error ^ ConstructionTest.java:18: error: cannot reference super before supertype constructor has been called super.i++; // Error ^ ConstructionTest.java:29: error: cannot reference this before supertype constructor has been called new Inner(); // Error - 'this' is enclosing instance ^ ConstructionTest.java:36: error: cannot reference this before supertype constructor has been called Inner.this.inner++; // Error - same instance 18

Slide 19

Slide 19 text

Quiz class C extends D { C() { super(this); // OK? } C(List list) { super((T)list.get(0)); // OK? } } ※ list はどこから来たとかOut Of Boundsは今回スルー(コンパイル時にOKかどうか) 19

Slide 20

Slide 20 text

Answer class C extends D { C() { super(this); // Error - refers to 'this' } C(List list) { super((T)list.get(0)); // Allowed - refers to 'T' but not 'this' } } this は初期化する前なのでNG。 T は型情報なのでOK 20

Slide 21

Slide 21 text

JEP 456: Unnamed Variables & Patterns 不要な変数やパターンをJava 9で利用禁止になった _ で書けるようにし、不要なこと を明確にしたり不要なネストを排することで可読性・保守性を向上させる JDK 21から変更なしで標準機能化 名前の順序が変わってる JEP 443: Unnamed Patterns and Variables (Preview) 1: JDK-8061549: Disallow _ as a one-character identifier https://bugs.openjdk.org/browse/JDK-8061549 21

Slide 22

Slide 22 text

Unnamed patterns (variable) OK // Before if (obj instanceof Point(int x, int y)) { System.out.println(y); /* x は不要*/ } // After if (obj instanceof Point(int _, int y)) { System.out.println(y); } if (obj instanceof Point(_, int y)) { System.out.println(y); } switch (o) { case Point(int x, _) -> ... } NG // Type o instanceof _ o instanceof _(int x, int y) case _ 22

Slide 23

Slide 23 text

Unnamed local variables, exception param, lambda param ブロック内のローカル変数宣言文 (JLS 14.4.2) var _ = hoge.inplaceMethod() (※ forなどのブロック内) try-with-resourceのリソース指定 (JLS 14.20.3) try (var _ = ScopedContext.acquire()){ // 獲得したリソースは使わない } for文のヘッダ (JLS 14.14.1, JLS 14.14.2) for (int i=0, _=sideEffect(); i<10; i++){...} for (Integer _: int){...} catchブロックのexception引数 (JLS 14.20) catch (Throwable _) { /* ignore */ } ラムダ式のパラメータ (JLS 15.27.1) ...stream.collect(Collectors.toMap(String::toUpperCase, _ -> "NODATA")) 23

Slide 24

Slide 24 text

JEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview) クラス、パッケージ、モジュールといった概念はなしでただ実行できるようにして、プ ログラムの基本(変数、分岐、ルーチン等)から段階的に学べるようにする JDK21では「JEP 445: Unnamed Classes and Instance Main Method(Preview)」 「無名クラス」ではなく、ホストにより選択された名前のクラスに変更 もともとの目的は他のクラスによって使用されないようにするため 無名クラスではなく通常のクラスと同様の扱いにして実装を簡易化 main methodの選択プロセスを簡易化 (parameterありかなしか、staticかinstance かを考慮する必要があった) String[] があればそのメソッドを呼び出す、なければパラメータを持たない メソッド候補を呼び出す static methodとinstance methodを同じ名前とシグネチャで宣言することはで きないのでこれで十分 24

Slide 25

Slide 25 text

Quiz どこまでOK? public static void main(String args[]) {} public static void main(String arg) {} public static void main() {} public void main(String args[]) {} protected void main(String args[]) {} private void main() {} void main() {} main() {} 25

Slide 26

Slide 26 text

Answer error: implicitly declared class does not have main method in the form of void main() or void main(String[] args) public static void main(String args[]) {} public static void main(String arg) {} // NG: `String[]` は必須 public static void main() {} public void main(String args[]) {} protected void main(String args[]) {} private void main() {} // NG: private はアクセスできない void main() {} main() {} // NG: class, interface, enum, or record expected 26

Slide 27

Slide 27 text

Before public class Main { public static void main(String[] args) { System.out.println("Hello, World!"); } } After void main() { System.out.println("Hello, World!"); } source-file modeで実行すればコンパイルすら要らない # ファイル名も何でもよい $ java --source 22 --enable-preview Test.java 27

Slide 28

Slide 28 text

クラス宣言がない+main methodがインスタンスメソッド宣言時の扱われ方 void main() { System.out.println("Hello, World!"); } new Object() { void main() { System.out.println("Hello, World!"); } }.main(); JLS 15.9.5. Anonymous Class Declarations と相当する 28

Slide 29

Slide 29 text

JEP 454: Foreign Function & Memory API 以下の既存APIの欠点を克服した「ヒープ外のメモリを直接扱うAPI」を提供 ByteBuffer でDirect Bufferは2GBまででメモリの開放はGCに依存する sun.misc.Unsafe はクラッシュの危険性があることと最終的には廃止させたい JNIはJavaで完結できずCコードを書く必要があり、CとJavaで行き来する必要があるの でオーバーヘッドが発生して性能が良くない 3回のPreviewを経て標準機能化。JAR-file manifestに Enable-Native-Access 追加などの 細かい修正あり 1: https://github.com/openjdk/jdk/commit/32ac72c3d35138f5253e4defc948304ac3ea1b53#diff- 56f51f9d0920f3600d9fffa99758c5066e8985a85ba6d28cbb321f6b6507ad34R721 2: https://github.com/openjdk/jdk/commit/32ac72c3d35138f5253e4defc948304ac3ea1b53 29

Slide 30

Slide 30 text

Survey どれを使っていますか? 1. ByteBuffer#allocateDirect 2. sun.misc.Unsafe 3. JNI (Java Native Interface) 4. JNA (Java Native Access) 5. JNR (Java Native Runtime) 30

Slide 31

Slide 31 text

puts(3) でHello, World!! public class FFMTest { public void puts(String message) { Linker linker = Linker.nativeLinker(); MethodHandle puts = linker.downcallHandle( linker.defaultLookup().find("puts").get(), FunctionDescriptor.ofVoid(ValueLayout.ADDRESS) ); try (Arena arena = Arena.ofConfined()) { MemorySegment segment = arena.allocateFrom(STR."\{message}\n"); puts.invoke(segment); } catch (Throwable _) { /* ignore */ } } public static void main(String[] args) { new FFMTest().puts(args[0]); } } $ java --enable-preview --enable-native-access=ALL-UNNAMED FFMTest "Hello, World\!\!" Hello, World!! # 注: `--enable-preview` はSTR template のため 31

Slide 32

Slide 32 text

道標: Function と Memory public void puts(String message) { // Java とNative を相互運用を提供するリンク Linker linker = Linker.nativeLinker(); // Function MethodHandle puts = linker.downcallHandle( linker.defaultLookup().find("puts").get(), FunctionDescriptor.ofVoid(ADDRESS) ); // Memory try (Arena arena = Arena.ofConfined()) { MemorySegment segment = arena.allocateFrom(STR."\{message}\n"); puts.invoke(segment); } catch (Throwable _) { /* ignore */ } } 32

Slide 33

Slide 33 text

Lookup // Java コードからNative コードへの呼び出し (downcall) MethodHandle puts = linker.downcallHandle( // プラットフォームのライブラリ(commonly used library) から linker.defaultLookup().find("puts").get(), // System#load またはSystem#loadLibrary でロードされたライブラリから // SymbolLookup.loaderLookup().find("puts"), // 指定したライブラリから // SymbolLookup.libraryLookup(path_or_name, Arena), FunctionDescriptor.ofVoid(ValueLayout.ADDRESS) ); NativeコードからJavaコードへの呼び出しは upcalls 。 Linker#upcallStub 33

Slide 34

Slide 34 text

// System#load またはSystem#loadLibrary でロードされたライブラリから System.loadLibrary("GL"); // libGL.so SymbolLookup LibGL = SymbolLookup.loaderLookup(); MemorySegment glGetString = libGL.find("glGetString").orElseThrow(); // 指定したライブラリから try (Arena arena = Arena.ofConfined()) { SymbolLookup libGL = SymbolLookup.libraryLookup("libGL.so", arena); MemorySegment glGetString = libGL.find("glGetString").orElseThrow(); ... } 34

Slide 35

Slide 35 text

Function (returning) pointer ValueLayout で表現する MethodHandle puts = linker.downcallHandle( linker.defaultLookup().find("puts").get(), // 戻り値がないのでofVoid, 引数はポインタ・配列: ADDRESS FunctionDescriptor.ofVoid(ValueLayout.ADDRESS) ); qsort の場合 ( ValueLayout は省略( import static ... )) void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); MethodHandle qsort = linker.downcallHandle( linker.defaultLookup().find("qsort").orElseThrow(), FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG, JAVA_LONG, ADDRESS) ); 35

Slide 36

Slide 36 text

戻り値が int で可変長引数を持つ printf の場合 int printf(const char *format, ...); printf("%d plus %d equals %d", 2, 2, 4); // char*, int, int, int MethodHandle printf = linker.downcallHandle( linker.defaultLookup().find("printf").orElseThrow(), FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_INT, JAVA_INT, JAVA_INT), Linker.Option.firstVariadicArg(1) // 最初のint は可変長(0 はフォーマット文字列) ); try (Arena arena = Arena.ofConfined()) { int res = (int)printf.invokeExact( // print "2 plus 2 equals 4" arena.allocateFrom("%d plus %d equals %d"), 2, 2, 4 ); } 36

Slide 37

Slide 37 text

37

Slide 38

Slide 38 text

道標: Function と Memory public void puts(String message) { // Java とNative を相互運用を提供するリンク Linker linker = Linker.nativeLinker(); // Function MethodHandle puts = linker.downcallHandle( linker.defaultLookup().find("puts").get(), FunctionDescriptor.ofVoid(ADDRESS) ); // Memory try (Arena arena = Arena.ofConfined()) { MemorySegment segment = arena.allocateFrom(STR."\{message}\n"); puts.invoke(segment); } catch (Throwable _) { /* ignore */ } } 38

Slide 39

Slide 39 text

Arena 一つ以上の MemorySegment のライフサイクルをモデル化 try (Arena arena = Arena.ofConfined()) { MemorySegment segment = arena.allocateFrom(STR."\{message}\n"); puts.invoke(segment); } catch (Throwable _) { /* ignore */ } // segment deallocated 種類 ライフタイム マルチスレッドアクセス Global 無限 可 Auto(matic) GC 可 Confined ユーザが制御 不可 Shared ユーザが制御 可 39

Slide 40

Slide 40 text

Arena Arenaが閉じられると全てのセグメントがアトミックに無効化される Shared arenaを閉じるとthread-local handshakeが起動する MemorySegment segment = null; try (Arena arena = Arena.ofConfined()) { segment = arena.allocateFrom(STR."\{message}\n"); puts.invoke(segment); } catch (Throwable _) { /* ignore */ } // segment deallocated segment.getString(0); // ==> illegalStateException: Already closed 40

Slide 41

Slide 41 text

MemorySegment メモリ割り当て。heap segment(on-heap)とnative segment(off-heap)の2種類ある on-heapは MemorySegment#ofArray() factory methodで取得し、off-heapは Arena#allocate factory methodで取得する try (Arena arena = Arena.ofConfined()) { // off-heap にallocate して MemorySegment segment = arena.allocateFrom(STR."\{message}\n"); // kick puts.invoke(segment); } catch (Throwable _) { /* ignore */ } 41

Slide 42

Slide 42 text

MemorySegment // int の場合 MemorySegment segment = arena.allocateFrom(JAVA_INT, 1); // アクセスも可能 int value = segment.get(JAVA_INT, 0); // 明示的にサイズを指定して確保し、Slicing も可能 MemorySegment segment = arena.allocate(100); // 100 bytes MemorySegment slice = segment.asSlice(50, 10); // offset:50, length:10 slice.get(JAVA_INT, 20); // read at offset:20 => Out of bounds 42

Slide 43

Slide 43 text

MemorySegment (on-heap) final byte[] ba = new byte[5]; ba[0] = 'h'; ba[1] = 'e'; ba[2] = 'l'; ba[3] = 'l'; ba[4] = 'o'; MemorySegment segment = MemorySegment.ofArray(ba); try { puts.invoke(segment); } catch (Throwable throwable) { throwable.printStackTrace(); } java.lang.IllegalArgumentException: Heap segment not allowed: MemorySegment{ heapBase: [B@442d9b6e, address: 0x0, byteSize: 100 } // * 注 at java.base/jdk.internal.foreign.abi.SharedUtils.checkNative(SharedUtils.java:320) at FFMTestOnHeap.puts(FFMTestOnHeap.java:26) at FFMTestOnHeap.main(FFMTestOnHeap.java:32) *注: 見易さのため改行を入れています 43

Slide 44

Slide 44 text

その他 MemoryLayout : MemorySegmentの構造体表現 typedef struct { char kind; int value; } TaggedValues[5]; SequenceLayout TAGGED_VALUES = MemoryLayout.sequenceLayout(5, MemoryLayout.structLayout( JAVA_BYTE.withName("kind"), MemoryLayout.paddingLayout(3), JAVA_INT.withName("value") ) ).withName("TaggedValues"); VarHandle : 型付けされた変数への参照 Linker#upcallStub : 外部関数にポインタを渡すことができる jextract : ライブラリとヘッダファイルを元にグルーコードを作成するツール 44

Slide 45

Slide 45 text

JEP 460: Vector API (Seventh Incubator) SIMD(Single Instruction, Multiple Data, 複数データに対して同じ操作を同時に実行すること を可能にし、1つのCPUサイクルでより多くの計算を実行できる仕組み)演算をサポートする ベクトル演算を実行するためのJava API ベクトル演算を最適なハードウェアレジスタとベクトル命令にコンパイルするC2コンパ イラ 計算爆発しないように様々な演算(単項演算、二項演算、変換演算等)に対応する汎 用化された内部演算を定義 JDK 22では API をマイナー強化 + バグ修正 + 性能改善 MemorySegment は byte 配列のみVectorアクセスができたが、ほかのプリミティ ブもサポートするように拡張された 45

Slide 46

Slide 46 text

使用例: c = (a*a + b*b) (※ 行計算) import jdk.incubator.vector.FloatVector; // SPECIES: 一度に計算するbit 数 // PREFERRED = そのハードウェアで使える最大値 (_MAX もある) import static jdk.incubator.vector.FloatVector.SPECIES_PREFERRED; void vectorComputation(float[] a, float[] b, float[] c) { int upperBound = SPECIES_PREFERRED.loopBound(a.length); for (int i=0; i < upperBound; i += SPECIES_PREFERRED.length()) { var va = FloatVector.fromArray(SPECIES_PREFERRED, a, i); var vb = FloatVector.fromArray(SPECIES_PREFERRED, b, i); var vc = va.mul(va).add(vb.mul(vb)); vc.intoArray(c, i); } } SPECIESは_64~_512まであるがハードウェアで対応していないbit数を使おうとするとソフ トウェア処理(=遅くなる)になるので注意 46

Slide 47

Slide 47 text

JEP 457: Class-File API (Preview) ASM と呼ばれるバイトコード操作・解析フレームワークが(JDK内部にも)ある クラスファイルフォーマットに強く依存するので互換性対応が大変 内部のASMも最新JDKに対応するまで遅延が生じるので、JDK Nで提供されるクラ スファイルの新機能を利用するにはJDK N+1以降となるケースがある クラスファイルAPIを提供することでJDK側が互換性を担保 ASM以外にもBCEL(Byte Code Engineering Library)やJavassistのようなクラスフ ァイル形式に強く依存しているフレームワークやツールにも有用 フィールド、メソッド、属性、バイトコード命令などのクラスファイルの実態を不 変のオブジェクトとして扱うAPI クラスファイルの階層的な構造を反映するためにツリー構造を採用 Class - Method - CodeAttribute - ... 1: https://asm.ow2.io/index.html 47

Slide 48

Slide 48 text

Java void fooBar(boolean z, int x) { if (z) foo(x); else bar(x); } ※ ここから「実例」まで、説明のためコードが一部簡略化されています 48

Slide 49

Slide 49 text

ASM ClassWriter classWriter = ...; MethodVisitor mv = classWriter.visitMethod(0, "fooBar", "(ZI)V", null, null); mv.visitCode(); mv.visitVarInsn(ILOAD, 1); Label label1 = new Label(); mv.visitJumpInsn(IFEQ, label1); mv.visitVarInsn(ALOAD, 0); mv.visitVarInsn(ILOAD, 2); mv.visitMethodInsn(INVOKEVIRTUAL, "Foo", "foo", "(I)V", false); Label label2 = new Label(); mv.visitJumpInsn(GOTO, label2); mv.visitLabel(label1); mv.visitVarInsn(ALOAD, 0); mv.visitVarInsn(ILOAD, 2); mv.visitMethodInsn(INVOKEVIRTUAL, "Foo", "bar", "(I)V", false); mv.visitLabel(label2); mv.visitInsn(RETURN); mv.visitEnd(); 49

Slide 50

Slide 50 text

Class-File API (java.lang.classfile ) ClassBuilder classBuilder = ...; classBuilder.withMethod("fooBar", MethodTypeDesc.of(CD_void, CD_boolean, CD_int), flags, methodBuilder -> methodBuilder.withCode(codeBuilder -> { Label label1 = codeBuilder.newLabel(); Label label2 = codeBuilder.newLabel(); codeBuilder.iload(1) .ifeq(label1) .aload(0) .iload(2) .invokevirtual(ClassDesc.of("Foo"), "foo", MethodTypeDesc.of(CD_void, CD_int)) .goto_(label2) .labelBinding(label1) .aload(0) .iload(2) .invokevirtual(ClassDesc.of("Foo"), "bar", MethodTypeDesc.of(CD_void, CD_int)) .labelBinding(label2); .return_(); }); 1: https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/lang/classfile/package-summary.html 50

Slide 51

Slide 51 text

Class-File API with lambda CodeBuilder classBuilder = ...; classBuilder.withMethod("fooBar", MethodTypeDesc.of(CD_void, CD_boolean, CD_int), flags, methodBuilder -> methodBuilder.withCode(codeBuilder -> { codeBuilder.iload(1) .ifThenElse( // <- b1 -> b1.aload(0) .iload(2) .invokevirtual(ClassDesc.of("Foo"), "foo", MethodTypeDesc.of(CD_void, CD_int)), b2 -> b2.aload(0) .iload(2) .invokevirtual(ClassDesc.of("Foo"), "bar", MethodTypeDesc.of(CD_void, CD_int))) .return_(); })); 51

Slide 52

Slide 52 text

Magic number CodeBuilder classBuilder = ...; classBuilder.withMethod("fooBar", MethodTypeDesc.of(CD_void, CD_boolean, CD_int), flags, methodBuilder -> methodBuilder.withCode(codeBuilder -> { int thisArg = codeBuilder.receiverSlot(); int boolArg = codeBuilder.parameterSlot(0); // boolean z int intArg = codeBuilder.parameterSlot(1): // int x codeBuilder.iload(boolArg) // Magic number ではなくなった .ifThenElse( b1 -> b1.aload(thisArg) // 同上 .iload(intArg) // 同上 .invokevirtual(ClassDesc.of("Foo"), "foo", MethodTypeDesc.of(CD_void, CD_int)), b2 -> b2.aload(thisArg) .iload(intArg) .invokevirtual(ClassDesc.of("Foo"), "bar", MethodTypeDesc.of(CD_void, CD_int))) .return_(); })); 52

Slide 53

Slide 53 text

クラスを実際に実装してみる 以下の Hello.class を実際に書いてみましょう public class Hello { public Hello() { } public static void main(String[] var0) { System.out.println("Hello World"); } } 53

Slide 54

Slide 54 text

import static java.lang.constant.ClassDesc.of; String string = "java.lang.String"; String system = "java.lang.System"; String printStream = "java.io.PrintStream"; // "Hello.class" に書き出すため`buildTo(..)` 。`build(..)` で`byte[]` を取得するのがよくある使い方 ClassFile.of().buildTo(Path.of("Hello.class"), of("Hello"), clb -> clb.withFlags(ACC_PUBLIC) .withMethod(INIT_NAME, MTD_void, ACC_PUBLIC, // INIT = instance initialization mb -> mb.withCode( cb -> cb.aload(cb.receiverSlot()) .invokespecial(CD_Object, INIT_NAME, MTD_void) .return_())) .withMethod("main", MethodTypeDesc.of(CD_void, of(string).arrayType()), ACC_STATIC + ACC_PUBLIC, mb -> mb.withCode( cb -> cb.getstatic(of(system), "out", of(printStream)) .ldc("Hello World") .invokevirtual(of(printStream), "println", MethodTypeDesc.of(CD_void, of(string))) .return_()))); 54

Slide 55

Slide 55 text

Transform 読み込んだクラス情報を書き換えることもできる。 debug から始まるメソッドを削除する例 ClassModel classModel = ClassFile.of().parse(bytes); byte[] newBytes = ClassFile.of().build(classModel.thisClass().asSymbol(), classBuilder -> { for (ClassElement ce : classModel) { if (!(ce instanceof MethodModel mm && mm.methodName().stringValue().startsWith("debug"))) { classBuilder.with(ce); } } }); 55

Slide 56

Slide 56 text

JEP 458: Launch Multi-File Source-Code Programs ソースコードから直接実行する Source-file mode が複数ファイルに対応した。JDK 11で入っ たJEP 330: Launch Single-File Source-Code Programsの発展型 # Before $ javac -d --class-path --source-path <.java file> $ java --class-path : # After $ java --class-path <.java file> jarファイルももちろん * で複数指定可能 56

Slide 57

Slide 57 text

流れ 1. ルートディレクトリを決定 2. モジュールを決定 i. ルートディレクトリにmodule-info.javaが存在する場合はそれに則る, OR ii. 無名モジュール 3. 指定ファイルと関連クラスをコンパイルし、クラスファイルをメモリ内キャッシュに格納 4. 起動クラスを決定 i. 指定ファイルの最初のトップレベルクラスがメインメソッドを持っていればそのクラス ii. 指定ファイルと同じ名前を持つクラスがメインメソッドを持っていればそのクラス iii. エラー 5. カスタムクラスローダー( MemoryClassLoader )により4のメインメソッドを呼び出す 1: https://github.com/openjdk/jdk/commit/517b1788198fc325961df61161f9b365c7b2524e 2: shebangで始まっている場合は1と2はskipされる ( --source-path が空扱い) 3: 以前の実装とは異なる。 Main から切り離されて com.sun.tools.javac.launcher.MemoryClassLoader と独自クラスになった 57

Slide 58

Slide 58 text

Quiz 以下の Test.java の実行結果は? public class Hoge { public static void main(String[] args) { System.out.println("Hoge"); } } public class Test { public static void main(String[] args) { System.out.println("Test"); } } public static void main(String[] args) { System.out.println("Declared"); } $ javac --enable-preview --source 22 Test.java $ java --enable-preview Test # コンパイルするので `--source` が必要 $ java --enable-preview --source 22 Test.java 58

Slide 59

Slide 59 text

1. Hoge 2. Test 3. Declared 4. それ以外 59

Slide 60

Slide 60 text

Answer どっちも3のDeclared $ javac --enable-preview --source 22 Test.java Note: Test.java uses preview features of Java SE 22. Note: Recompile with -Xlint:preview for details. $ java --enable-preview Test Declared # 念のためクラスファイルは削除 $ java --enable-preview --source 22 Test.java Declared 60

Slide 61

Slide 61 text

Quiz public class Hoge { public static void main(String[] args) { System.out.println("Hoge"); } } public class Test { public static void main(String[] args) { System.out.println("Test"); } } $ javac --enable-preview --source 22 Test.java $ java --enable-preview Test # コンパイルするので `--source` が必要 $ java --enable-preview --source 22 Test.java 61

Slide 62

Slide 62 text

1. Hoge 2. Test 3. Declared 4. それ以外 62

Slide 63

Slide 63 text

Answer 4のそれ以外と1のHoge $ javac --enable-preview --source 22 Test.java Test.java:1: error: class Hoge is public, should be declared in a file named Hoge.java public class Hoge { ^ エラー1 個 $ java --enable-preview --source 22 Test.java Hoge 流れ のセクションで説明した通り、source-file modeではファイル名で一致したクラスでは なく、最初のトップレベルクラスが優先して選ばれる。普通にコンパイルするとエラー。 63

Slide 64

Slide 64 text

JEP 459: String Templates (Second Preview) 変数を含む文字列を書きやすくしよう!が目的。今までは以下のように記述が必要だった // 読みづらい String s = x + " plus " + y + " equals " + (x + y); // 細かすぎる s = new StringBuilder() .append(x) .append(" plus ") .append(y) .append(" equals ") .append(x + y) .toString(); // 引数の数や型の不一致を招きやすい s = String.format("%2$d plus %1$d equals %3$d", x, y, x + y); s = "%2$d plus %1$d equals %3$d".formatted(x, y, x + y); // 独特すぎる MessageFormat mf = new MessageFormat("{0} plus {1} equals {2}"); s = mf.format(x, y, x + y); 64

Slide 65

Slide 65 text

今後はimport不要な組み込みの STR を使って書きやすくなります var x = 10.0; var y = 20.5; // 変数は \{ で始まり } で閉じる String s = STR."\{ x } plus \{ y } equals \{ x + y }"; s ==> "10.0 plus 20.5 equals 30.5" 文字列変換前のデータ構造がそのまま返る RAW も用意されている StringTemplate.RAW."\{ x } plus \{ y } equals \{ x + y }"; $1 ==> StringTemplate{ fragments = [ "", " plus ", " equals ", "" ], values = [10.0, 20.5, 30.5] } // * 注 *注: 見易さのため改行を入れています 65

Slide 66

Slide 66 text

Text Block Text Blockも同じように使うことができる s = STR.""" \{ x } plus \{ y } equals \{ x + y} """; s ==> "10.0\n plus\n20.5\n equals\n30.5\n" 66

Slide 67

Slide 67 text

Formatter Formatterも StringTemplate.Processor の1実装である java.util.FormatProcessor を 利用して同じような感じで表現が可能 FormatProcessor.FMT."%2.1f\{ x } plus %2.2f\{ y } equals %.3f\{ x + y }"; $1 ==> "10.0 plus 20.50 equals 30.500" // 16 進 var x = 10; var y = 20; FormatProcessor.FMT."0x%04x\{x} + 0x%04x\{y} = 0x%04x\{x + y}"; $2 ==> "0x000a + 0x0014 = 0x001e" JDK 22ではTemplate式の型( StringTemplate.Processor )が明文化された (JLS 15.8.6) が、仕様上の変更であり実際の使い勝手は変わらない。 67

Slide 68

Slide 68 text

Advanced usage StringTemplate.Processor JSON = StringTemplate.Processor.of( // 単純にJsonObject を作成して返しているがValidation 処理を加えたりもできる (StringTemplate st) -> new JsonObject(st.interpolate()) ); String name = "Joan Smith"; String phone = "555-123-4567"; String address = "1 Maple Drive, Anytown"; // Template 形式でString を書きつつJsonObject を作成する JsonObject doc = JSON.""" { "name": "\{name}", "phone": "\{phone}", "address": "\{address}" }; """; Robustなパーサーが@shinyafoxさんより実装された: https://github.com/bitterfox/json- string-template 68

Slide 69

Slide 69 text

JEP 462: Structured Concurrency (Second Preview) 並列処理を楽にしたい 「タスクをあるコードブロックでサブタスクに分けて同時進行できるようにし、最 終的にそれらは同じコードブロックに戻る」仕組みで実現 JDK 21から変更なし JDK 21 で StructuredTaskScope#fork(...) が Future ではなく Subtask を 返すように変更されたので更なるフィードバックを得るためPreviewリトライ JDK 19から続いている (最初はIncubator) 69

Slide 70

Slide 70 text

// サブタスクで実行させる処理のリスト List> tasks = ... try (var scope = new StructuredTaskScope()) { // scope.fork() によりサブタスクを実行( フォーク) List> subtasks = tasks.stream().map(scope::fork).toList(); // サブタスクの完了を待機 scope.join(); // 成功サブタスクと失敗サブタスクを別セットに分割 Map>> map = subtasks.stream()  .collect(Collectors.partitioningBy( h -> h.state() == Subtask.State.SUCCESS, Collectors.toSet())); } // 全リソースクローズ 70

Slide 71

Slide 71 text

実行中のスレッドいずれかが返ってきたら他をすべてシャットダウン ShutdownOnSuccess を利用する try (var scope = new StructuredTaskScope.ShutdownOnSuccess()) { List> subtasks = tasks.stream().map(scope::fork).toList(); // いずれか一つのサブタスクが成功(Subtask.State#SUCCESS) するか、 // すべて失敗(Subtask.State#FAILED) するまで待つ scope.join(); try { // 最初に成功したサブタスクの結果を取得 String result = scope.result(); ... // 成功したタスクの結果を利用した処理 } catch (ExecutionException e) { // すべて失敗した場合はExecutionException が投げられる throw new CustomException(e); } } 71

Slide 72

Slide 72 text

実行中のスレッドいずれかが例外を出したらすべてシャットダウン ShutdownOnFailure を利用する try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { // 結果が欲しいだけなのでSupplier を使う List> suppliers = tasks.stream().map(scope::fork).toList(); // いずれか一つのサブタスクが失敗するか、すべて成功するか、10 秒経過するまで待つ // (10 秒過ぎたらTimeoutException) Instant deadline = LocalDateTime.now().plusSeconds(10).toInstant(ZoneOffset.UTC); scope.joinUntil(deadline); // 最初に失敗したサブタスクの例外を補足して伝播、終了 scope.throwIfFailed(e -> new CustomException(e)); // 全てのサブタスクが成功した場合の処理 ( サブタスクの結果(String) を並べてるだけ) String result = suppliers.stream() .map(Supplier::get) .collect(Collectors.joining(", ")); } 72

Slide 73

Slide 73 text

JEP 464: Scoped Values (Second Preview) スレッド内と子スレッドの両方で不変データを共有するプログラミングモデルを提供。 java.lang.ScopedValue とくに大量のVirtual threadsを使用する場合において、ThreadLocal変数よりも、使いや すさ・理解しやすさ、堅牢性や性能の向上を目指した JDK 21から変更なし 73

Slide 74

Slide 74 text

ThreadLocal変数の設計上の欠陥 1. 変更可能性:ThreadLocal変数はいつでも変更可能であり、データフローがスパゲッティ 化し、どのコンポーネントがどの順序で(共有している)状態を更新しているのかが不明瞭 になる可能性がある 2. 長命:ThreadLocal変数はスレッドの寿命が尽きるまで、または remove() メソッドが 呼び出されるまで保持され、しばしば開発者は忘れる(か、1.のせいで複雑すぎて不要に なるタイミングが分からない)ため、メモリリークする可能性がある 3. 継承:親スレッドのThreadLocal変数が子スレッドに継承されるため、多くのスレッド を使う場合にオーバーヘッド(特にメモリ使用量)が増大する可能性がある 74

Slide 75

Slide 75 text

Before void dukeAdventure() { User user = new User("duke"); process(user); } void process(User user){ // このメソッドではUser は使わないが後続処理のために引数に必要 ... // 何らかの処理 subsequentProcess(user); } void subsequentProcess(User user){ ... // User を使った処理 } 75

Slide 76

Slide 76 text

Before(ThreadLocal) final ThreadLocal USER = new ThreadLocal<>(); void dukeAdventure() { USER.set(new User("duke")); process(); } void process(){ // 引数が不要になった ... subsequentProcess(); } void subsequentProcess(){ // User が必要な時に get する User user = USER.get(); ... // User を使った処理 } 76

Slide 77

Slide 77 text

After final ScopedValue USER = ScopedValue.newInstance(); // 引数は ScopedValue のkey, value, Runnable // 渡したRunnable が完了したら即座に解放される ScopedValue.runWhere(USER, new User("duke"), () -> subsequentProcess()); void subsequentProcess(){ User user = USER.get(); // duke ... } 77

Slide 78

Slide 78 text

Rebinding void subsequentProcess() { User user = USER.get(); // duke ... // User に対する処理 // 再バインド (userService から最新のユーザ情報を入手してバインドする例) ScopedValue.runWhere(USER, userService.get(), () -> doAnother() ); // doAnother() が完了すると元の"duke" ユーザにバインドされ直される } void doAnother() { User user = USER.get(); // userService.get() で得たユーザ ... } 78

Slide 79

Slide 79 text

Inheriting Scoped Values // StructuredTaskScope によって生成された子スレッドは、親スレッドのスコープ値にアクセス可能 // ThreadLocal の場合は、その値が各子スレッドにコピーされてしまっていた(= メモリがどーん) ScopedValue.runWhere(USER, new User("duke"), () -> { try (var scope = new StructuredTaskScope()) { scope.fork(() -> childTask()); ... } }); String childTask() { // 使い勝手は同じ User user = USER.get(); // duke ... } 79

Slide 80

Slide 80 text

JEP 461: Stream Gatherers (Preview) これまで用意されていたもの(例: .map(..) , .filter(..) )しか使えなかったStream の中間操作(intermediate operations)のユーザ定義をサポート 終端操作(terminal operations)の java.util.stream.Collector に対する java.util.stream.Gatherer stream.gather(..).gather(..).gather(..).collect(..); andThen(..) がサポートされているので繋げられる built-in gathererは java.util.stream.Gatherers にある 中間操作はこれまでいくつも提案されてきたが、全部追加すると学習コストが急増する カスタム中間操作をどのように形で追加すべきか検討してきたが、終端操作と同じ アプローチでサポートを決定 80

Slide 81

Slide 81 text

built-in gatherers fold : many-to-one. 入力要素がなくなるまでインクリメントに集約(aggregate). Stream.of(1,2,3,4,5) .gather( Gatherers.fold( () -> "", // initial (current, next) -> { // folder if (!current.isEmpty()) return current + "." + next; return next.toString(); } ) ).toList(); // "," にしたら List と見分けがつかなかったので"." を採用 $1 ==> [1.2.3.4.5] // initial を `() -> "0"` にした場合 $2 ==> [0.1.2.3.4.5] Stream#reduce とは、 (1)順序保証がある (2) BiFunction を引数にするので入力と結果の型 が同じでなくともよい という違いがある。 81

Slide 82

Slide 82 text

built-in gatherers scan : one-to-one. インクリメンタルな累積(accumulation). Stream.of(1,2,3,4,5) .gather( Gatherers.scan( () -> "", // initial (current, next) -> current + next)) // scanner .toList(); $1 ==> [1, 12, 123, 1234, 12345] 82

Slide 83

Slide 83 text

Quiz initialの処理を変えました。どうなるでしょうか? Stream.of(1,2,3,4,5) .gather( Gatherers.scan( // 前: () -> "", () -> 10, // initial (current, next) -> current + next)) // scanner .toList(); $1 ==> [1, 12, 123, 1234, 12345] # 前の結果 ( 参考用) $2 ==> [10, 1, 12, 123, 1234, 12345] # 最初の要素が10 $3 ==> [11, 22, 133, 1244, 12355] # 10 + 前の結果 $4 ==> [101, 1012, 10123, 101234, 1012345] # "10" + 前の結果 $5 ==> それ以外 83

Slide 84

Slide 84 text

Answer $5 ==> [11, 13, 16, 20, 25] 原因: () -> "", 84

Slide 85

Slide 85 text

current (現在の状況)を出力すると分かりやすい Stream.of(1,2,3,4,5).gather(Gatherers.scan( () -> 10, (current, next) -> { System.out.println(current); return current + next; })).toList(); 10 // 10 (initial) 11 // 10 + 1 13 // 11 + 2 16 // 13 + 3 20 // 16 + 4 $1 ==> [11, 13, 16, 20, 25] // () -> "" の場合 // "" (initial) 1 // "" + 1 12 // "1" + 2 ( なぜなら "" + 1 = "1" だから) 123 // "12" + 3 1234 // "123" + 4 $2 ==> [1, 12, 123, 1234, 12345] 85

Slide 86

Slide 86 text

built-in gatherers mapConcurrent : one-to-one gatherer. Virtual Threadを使って第一引数 ( maxConcurrency )まで並行に mapper を実行。順序は保持される windowFixed : many-to-many. 順番に詰める windowSliding many-to-many. 一つずつずらしながら末尾まで詰める Stream.of(1,2,3,4,5).gather(Gatherers.windowFixed(3)).toList(); $1 ==> [[1, 2, 3], [4, 5]] Stream.of(1,2,3,4,5).gather(Gatherers.windowSliding(3)).toList(); $2 ==> [[1, 2, 3], [2, 3, 4], [3, 4, 5]] 86

Slide 87

Slide 87 text

Before windowFixed をterminal operations( Collector )で実装 Stream.iterate(1, i->++i).limit(5) // Stream.of(1,2,3,4,5) .collect(Collector.of( () -> new ArrayList>(), (groups, element) -> { if (groups.isEmpty() || groups.getLast().size() == 3) { var current = new ArrayList(); current.add(element); groups.addLast(current); } else { groups.getLast().add(element); } }, (left, right) -> { throw new UnsupportedOperationException("Cannot be parallelized"); } )); $3 ==> [[1, 2, 3], [4, 5]] 87

Slide 88

Slide 88 text

もちろん builtin だけではなく Gatherer で自前実装もできる Stream.iterate(1, i->++i).limit(5).gather(Gatherer.ofSequential( // Initializer: creation of a new, potentially mutable, state () -> new ArrayList(), // Integrator: 引数はstate, element, result Gatherer.Integrator.of((window, element, downstream) -> { window.add((Integer)element); // state に今のelement を入れる if(window.size() == 3) { // 3 つ溜まったらresult に入れる final var oldWindow = window.clone(); // shallow copy window.clear(); // state を初期化 return downstream.push(oldWindow); } return true; // integration が続くかを示す。true は継続、false は中断 }), // Combiner: BinaryOperator を実装する。今回はSequential なので実装しない( できない) // Sequential でないケースでは並列性(parallel().reduce() 相当) のため // `Gatherer.of(..)` を利用して実装するのが良い // Finisher: 最後の処理。ここでは残りを result に入れている (window, downstream) -> { downstream.push(window); } )).toList(); 88

Slide 89

Slide 89 text

正実装 java.base/share/classes/java/util/stream/Gatherers.java public static Gatherer> windowFixed(int windowSize) { if (windowSize < 1) throw new IllegalArgumentException("'windowSize' must be greater than zero"); class FixedWindow { Object[] window; int at; FixedWindow() { at = 0; window = new Object[windowSize]; } boolean integrate(TR element, Downstream> downstream) { window[at++] = element; if (at < windowSize) { return true; } else { final var oldWindow = window; window = new Object[windowSize]; at = 0; return downstream.push( SharedSecrets.getJavaUtilCollectionAccess() .listFromTrustedArrayNullsAllowed(oldWindow) ); } } 89

Slide 90

Slide 90 text

void finish(Downstream> downstream) { if (at > 0 && !downstream.isRejecting()) { var lastWindow = new Object[at]; System.arraycopy(window, 0, lastWindow, 0, at); window = null; at = 0; downstream.push( SharedSecrets.getJavaUtilCollectionAccess() .listFromTrustedArrayNullsAllowed(lastWindow) ); } } } return Gatherer.>ofSequential( // Initializer FixedWindow::new, // Integrator Integrator.>ofGreedy(FixedWindow::integrate), // Finisher FixedWindow::finish ); } 90

Slide 91

Slide 91 text

JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview) JDK 23. パターンコンテキストでプリミティブ型を使えるようにして instanceof と switch を拡張。Value ObjectとPrimitiveを統合するより先にパターンを対応。 switch (x.getStatus()) { case 0 -> "okay"; case 1 -> "warning"; case 2 -> "error"; case int i -> "unknown status: " + i; } // 三項演算子代わり handle(someParameter, switch (x.getStatus().isOkay()) { case true -> x.getStatus(); case false -> { log("Status is not okay"); yield -1; } }); 91

Slide 92

Slide 92 text

Head toward Java 22 (and Java 23) KUBOTA Yuji LINEヤフー株式会社 Japan Java User Group Night Seminar 2024/03/27 92