JJUG CCC 2022 Fall ( #jjug_ccc ) - セッション資料の一覧

JJUG CCC 2022 Fall に参加しました!

オンライン開催も、今回で4回目。
今回はスタッフとして参加1して、ちょっとだけ司会をしました。スムーズに進行できていたら幸いです。

今回、特によかったなーと思ったのが、完成度の高いもちださんのセッション。
まさかのバーチャルお嬢様の声でセッションが始まり、最後にはみなさんの感想ツイートが「~ですわ」になっていたのは面白かったですw
内容も Virtual Thread に関してはこれみれば一通り理解できるという充実度で、しかも動きもついて分かりやすい内容でした。あとで動画が公開されたら真っ先にみてみるといいかと思います!

あと、数村さんの コンテナ環境でのJava技術の進化 で紹介されていた Container Restore In Userspace (CRIU) が気になりました。
プロセスを止めて再開する技術ですが、Java でもできないか検討されているという事。今後どうなっていくのか楽しみです。

さて、次回は JJUG CCC 2023 Spring。オフラインで、もっと人が集まれるようにするかも?とのこと。
オンライン開催も定着してきたとはいえ、以前のオフラインのお祭り感も好きです。なので、ぜひ会場でお会いしましょう!2


さて、最後にいつものを。

今回、残念ながら時間がかぶってしまって参加できなかったセッションがいっぱいあったので、あとで読むために現時点で発表者の方が公開されている資料一覧をまとめしました。3
(あとで JJUG CCC 2022 Fall のページにもリンクが載るかもしれませんが、とりあえず自分の方で調べました)

10:00

11:00

12:30

13:30

15:00

16:00

17:00

  • A: Maven Puzzlers / Andres Almiray
  • B: Persistence made easy with Jakarta Data & NoSQL / Otavio Santana
  • C: Helidon Reactive vs. Blocking (Níma) / Mitia Alexandrov
  • D: PostgreSQL, The Time-Series Database You Want / Chris Engelbert

  1. スタッフ参加・一般参加という言い方を個人的にしていたんですが、もしかしてこれってコミケ用語…?
  2. LTを用意しなきゃかな…
  3. 発表者のお名前は敬称略とさせていただきました。

Jextract を使って、Java から libcurl を使ってみる

Jextract に libcurl を呼び出すサンプルがありました。これを試しに動かしてみました。 (まだ PreviewForeign Function & Memory API を使います)

ちなみに、ほかにも以下のライブラリを使うサンプルが置いてあります。

ディレクト 実行するライブラリ
cblas OpenBLAS(行列演算ライブラリ)
lapack LAPACK (数値解析ソフトウェアライブラリ)
libclang Clang の C インタフェースライブラリ
libcurl cURL(データ転送ライブラリ)
libffmpeg FFmpeg(動画・音声の記録・変換・再生ライブラリ)
libgit2 Git(バージョン管理)
libjimage ImageJ(画像処理ライブラリ)
libproc /proc インターフェイス
lp_solve lp_solve(混合整数線形計画法のソルバー)
opengl OpenGL(2次元/3次元コンピュータグラフィックスライブラリ)
readline Readline(コマンドライン対話ライブラリ)
sqlite SQLite(データベース)
tcl Tcl(ユーティリティモジュール)
tensorflow TensorFlow(機械学習ライブラリ)
dlopen 動的リンクを行うローダー
time C言語標準ライブラリの time

また、Python3 スクリプトを実行するサンプル、Go との相互呼び出しを行うサンプルもおいてあります。

前置き

Jextract とは

jextract is a tool which mechanically generates Java bindings from a native library headers. This tools leverages the clang C API in order to parse the headers associated with a given native library, and the generated Java bindings build upon the Foreign Function & Memory API.

jextract は、ネイティブ ライブラリ ヘッダーから Java バインディング機械的に生成するツールです。このツールは、clang C API を利用して、特定のネイティブ ライブラリに関連付けられたヘッダーを解析し、生成された Java バインディングForeign Function & Memory API に基づいて構築されます。

README.md

Foreign Function & Memory API とは

Introduce an API by which Java programs can interoperate with code and data outside of the Java runtime. By efficiently invoking foreign functions (i.e., code outside the JVM), and by safely accessing foreign memory (i.e., memory not managed by the JVM), the API enables Java programs to call native libraries and process native data without the brittleness and danger of JNI. This is a preview API.

Java プログラムが Java ランタイム外のコードやデータと相互運用できる API を導入します。外部関数 (つまり、JVM の外部のコード) を効率的に呼び出し、外部メモリ (つまり、JVM によって管理されないメモリ) に安全にアクセスすることにより、APIJava プログラムがネイティブ ライブラリを呼び出し、ネイティブ データをもろくて危険なJNIを使わずに処理できるようにします。 これはプレビュー API です。

Introduction

また、先日の JJUG ナイトセミナーの資料も併せてごらんください。 JEP 424 Foreign Function & Memory API を試しに使ってみました! - Speaker Deck

今回動かすコード

jextract の samples/libcurl/CurlMain.java 引数の URL にアクセスして、取得した内容を標準出力に流すプログラムです。

import java.lang.foreign.MemorySession;
import java.lang.foreign.SegmentAllocator;
import static java.lang.foreign.MemoryAddress.NULL;
import static org.jextract.curl_h.*;
import org.jextract.*;

public class CurlMain {
   public static void main(String[] args) {
       var urlStr = args[0];
       curl_global_init(CURL_GLOBAL_DEFAULT());
       var curl = curl_easy_init();
       if(!curl.equals(NULL)) {
           try (var session = MemorySession.openConfined()) {
               var url = session.allocateUtf8String(urlStr);
               curl_easy_setopt(curl, CURLOPT_URL(), url.address());
               int res = curl_easy_perform(curl);
               if (res != CURLE_OK()) {
                   String error = curl_easy_strerror(res).getUtf8String(0);
                   System.out.println("Curl error: " + error);
                   curl_easy_cleanup(curl);
               }
           }
       }
       curl_global_cleanup();
   }
}

手順

下準備(jextract のコンパイル

Ubuntu 22.04 で作業しました。 WSL2 上でも同じはず。 Mac の場合は README.md を参照。

apt で必要なものをインストールします。

  • Gradle 実行用の Java 171
  • jextract のコンパイルに必要な LLVM, clang
  • サンプルの実行に必要な libcurl (with OpenSSL)
sudo apt update
sudo apt install -y openjdk-17-jdk llvm-dev libclang-14-dev libcurl4-openssl-dev

JAVA_HOME を設定します。

export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64/

続いて、Java 19 を準備します。 どのディストリビューションでも構わないですが、今回は https://jdk.java.net/19/ のものを使用しました。

cd $HOME
curl -LO 'https://download.java.net/java/GA/jdk19/877d6127e982470ba2a7faa31cc93d04/36/GPL/openjdk-19_linux-x64_bin.tar.gz'
tar xf openjdk-19_linux-x64_bin.tar.gz

jextract をコンパイルします。

git clone git@github.com:openjdk/jextract.git
sh ./gradlew -Pjdk19_home=$HOME/jdk-19/ -Pllvm_home=/usr/lib/llvm-14 clean verify

ところが、エラーになりました。

> Error: the path /usr/lib/llvm-14/lib/clang/14/include does not exist

どうやら、パスが違うみたいです。パッチを当てます。

diff --git a/build.gradle b/build.gradle
index 1e8b620..c1b3cd7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -20,7 +20,7 @@ if (clang_versions.length == 0) {
     throw new IllegalArgumentException("Could not detect clang version." +
             " Make sure a ${llvm_home}/lib/clang/<VERSION> directory exists")
 }
-def clang_version = clang_versions[0]
+def clang_version = clang_versions[1]

 def jextract_version = "19"
 def jmods_dir = "$buildDir/jmods"

改めて実行します。

sh ./gradlew -Pjdk19_home=$HOME/jdk-19/ -Pllvm_home=/usr/lib/llvm-14 clean verify

今度はうまくいきました。

BUILD SUCCESSFUL in 38s
8 actionable tasks: 8 executed

jextract コマンドにパスを通しておきましょう。

export PATH=`pwd`/build/jextract/bin:$PATH 

サンプルの実行

まずは、jextract で curlバインディング用クラスを生成します。

cd samples/libcurl
jextract -t org.jextract -lcurl /usr/include/x86_64-linux-gnu/curl/curl.h

これで、 org/jextract 配下にクラスが作られます。 (ちなみに、上記のコマンドに --source を付与すれば、クラスのソースコードが作られます)

それでは、いよいよサンプルの実行です!

java --enable-native-access=ALL-UNNAMED \
    --enable-preview --source=19 \
    -Djava.library.path=/usr/lib/x86_64-linux-gnu CurlMain.java \
    'http://www.example.com/'

うまくいけば、がーっと html が流れてきます。

Note: CurlMain.java uses preview features of Java SE 19.
Note: Recompile with -Xlint:preview for details.
<!doctype html>
<html>
<head>
    <title>Example Domain</title>

    <meta charset="utf-8" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
(略)

まとめ

Java からいろいろなライブラリを呼べるようになって、Java の可能性が一気に広がった感じがします。


  1. Gradle がまだ Java 19 未サポートなので別のバージョン (Java 17) をここでインストールしています。

Java の Charset のバイトオーダーとBOM

Java のサポートされているエンコーディングの一覧を見ていると、UTF-16, UTF-16BE, UTF-16LE, x-UTF-16LE-BOM とあって、なにが違うんだろうと思って調べてみました。

まとめると、読み・書き時のバイトオーダー (BO) 1 と書き込み時の BOM (Byte Order Mark) の違いでした。

  • 読み込み時のバイトオーダー
    • BE / LE が付かないものは、BOM から自動的に判定
  • 書き込み時のバイトオーダー
    • BE / LE が付かないものは、BIG バイトオーダー
  • BOM の有無
    • UTF-8 に「BOM あり」で書き込むものはない2
名称 読み込み時
バイトオーダー
書き込み時
バイトオーダー
書き込み時
BOM
UTF-8 - - x
UTF-16 AUTO BIG o
UTF-16BE BIG BIG x
UTF-16LE LITTLE LITTLE x
x-UTF-16LE-BOM AUTO LITTLE o
UTF-32 AUTO BIG x
UTF-32BE BIG BIG x
X-UTF-32BE-BOM BIG BIG o
UTF-32LE LITTLE LITTLE x
X-UTF-32LE-BOM LITTLE LITTLE o

  1. バイトオーダー = エンディアン

  2. バイトオーダーに関わらず同じ内容になるので、本来は UTF-8 に BOM (Byte Order Mark) は不要

年/月/日 の一部が省略された日付をパースして LocalDateTime を取得する方法

年が省略されている日付(月/日)の場合

まずは、DateTimeFormatter を用意します。

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM/dd");

次に、日付を MonthDay クラスでパースします。

MonthDay monthDay = MonthDay.parse("07/18", formatter)
// ==> --07-18

それに、 現在の年を付け加えます。

LocalDate date = Year.now().atMonthDay(monthDay);
// ==> 2022-07-18

最後に、LocalDate#atStartOfDay() で日付を 00:00:00 に設定すれば、LocalDateTime クラス が取得できます。

LocalDateTime dateTime = date.atStartOfDay();
// ==> 2022-07-18T00:00

日が省略されている日付(年/月)の場合

YearMonth クラスを使うぐらいで、あとは月日の場合と大体同じです。

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu/MM")

YearMonth yearMonth = YearMonth.parse("2022/07", FORMATTER);
// ==> 2022-07

LocalDate date = yearMonth.atDay(1);
// ==> 2022-07-01

LocalDateTime dateTime = date.atStartOfDay();
// ==> 2022-07-01T00:00

左腕のハードウェア故障についてのご報告

6月6日(土)に自転車でお散歩していた際に転倒し、左腕がクラッシュしました。 それにより、しばらくの間は右腕のみの片系運用となっておりました。

数日が経過しても左腕からのアラートが治まらない状態が続いたことから、病院に詳細なデバッグを依頼したところ、 ハードウェア故障(左肘頭骨折)が発覚しました。

対応

意識を運用から切り離しての修復作業全身麻酔下での手術)が行われました。
これにより、現在はハードウェア内部で故障個所をワイヤーで固定した仮復旧状態となっております。
(負荷をかけなければ、基本的な動作に支障はありません)

なお、完全な修復までには、もう2ヶ月ほど時間がかかる見込みです。

フォロワーの皆様には、ご心配をおかけいたしました。

今後について

自転車でお散歩する際、何らかの原因による転倒は避けられないものと考えております。 そのため、もし今後転倒した際にはしっかりと受け身の姿勢で無難にやり過ごすように対応するつもりです。

以上

JJUG CCC 2022 Spring ( #jjug_ccc ) - セッション資料の一覧

JJUG CCC 2022 Spring に参加しました!

いつも以上におもしろいセッション盛りだくさんで、参加してとても楽しかったです。
LINEのトラブルシューティングのような現場での経験をもとにした話から、バイトコードのようなJavaのコアな話まで、とてもバランスよくセッションが採択されていたからかなと思います。
特に前者はなかなか普段の JJUG ナイトセミナーなどでは聞けないので、いつもとても楽しみにしています。

あと、動画セッションならではですが、今回は休憩時間中に時間がかぶって見れなかったセッションを2倍速で追っかけて見るということをやってみて、より密度の高い時間になりました。
授業は動画配信で見たいという大学生の気持ちがちょっとわかりましたw

次回の JJUG CCC 2022 Fall も楽しみです。
もしかすると、オフラインも併用になるんでしょうか。ただ、飲食を伴う懇親会はまだ厳しそう…?


さて、最後にいつものを。

今回、残念ながら時間がかぶってしまって参加できなかったセッションがいっぱいあったので、あとで読むために現時点で発表者の方が公開されている資料一覧をまとめしました。1
(あとで JJUG CCC 2022 Spring のページにもリンクが載るかもしれませんが、とりあえず自分の方で調べました)

10:00

11:00

12:30

13:30

15:00

16:00

17:00

過去記事


  1. 発表者のお名前は敬称略とさせていただきました。

Java のラムダ式のクラスファイルを調べてみた!

Javaラムダ式が、どのようにコンパイルされ実行されているか気になりますよね。 そこで、クラスファイルを分析して、その中身を調べてみました。

なお、今回は OpenJDK 17.0.1 を使って調べています。 バージョンによって挙動が異なる場合があるので、ご注意ください。

外側のローカル変数を参照しない場合

おさらい:匿名クラスのコンパイル結果

本題のラムダ式を調べてみる前に、匿名クラスの場合にどのようにコンパイルされているかを確認しておきましょう。 (ご存じの方は、読み飛ばしてもらって大丈夫です)

今回は、Runnable インタフェースを匿名クラスとして実装しました。

public class Main {
  public static void main(String[] args) throws InterruptedException {
    var thread = new Thread(new Runnable() {
      @Override
      public void run() {
        System.out.println("Hello world!");
      }
    });

    thread.run();
    thread.join();
  }
}

このクラスをコンパイルすると、2つのクラスファイルが出来上がりました。

  • Main.class
  • Main$1.class

後者が匿名クラスのコンパイル結果です。 これを、OpenJDK 付属の javap でリバースアセンブルしてみました。

Classfile /C:/Users/YujiSoftware/Desktop/java/Main$1.class
  Last modified 2021/12/18; size 534 bytes
  SHA-256 checksum f6f8ec5118c480c54b31ee06db110837e3459fa50f8d052a7d157b32885fd22d
  Compiled from "Main.java"
class Main$1 implements java.lang.Runnable
  minor version: 0
  major version: 61
  flags: (0x0020) ACC_SUPER
  this_class: #21                         // Main$1
  super_class: #2                         // java/lang/Object
  interfaces: 1, fields: 0, methods: 2, attributes: 4
{
  Main$1();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public void run();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #13                 // String Hello world!
         5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
}

この javap の結果を、@YujiSoftware を使って Javaソースコードに変換してみました。 1

class Main$1 implements java.lang.Runnable {
  Main$1(){
    super();
  }
  
  public void run() {
    System.out.println("Hello world!");
  }
}

「外側のクラス名 + $ + 連番」という名前のクラスです。 クラスの中身は、普通の(匿名ではない)クラスとして実装したのと同じです。

なお、Java 言語仕様に基づいて、匿名コンストラクタが追加されています。

:::note info Java言語仕様:15.9.5.1 匿名コンストラクタ 】 匿名クラスは、明示的にコンストラクタを宣言することができない。その代わりに、コンパイラは匿名クラスに対して匿名コンストラクタ(anonymous constructor)を自動的に提供しなければならない。 :::

ラムダ式コンパイル結果

続いて、ラムダ式の場合を見ていきましょう。 先ほどの匿名クラスを使って書いた部分を、ラムダ式に置き換えました。

public class Main {
  public static void main(String[] args) throws InterruptedException {
    var thread = new Thread(() -> System.out.println("Hello world!"));

    thread.run();
    thread.join();
  }
}

このクラスをコンパイルすると、1つのクラスファイルが出来上がりました。

  • Main.class

匿名クラスの時のように、コンパイル時にはクラスファイルは作られませんでした。 代わりに、 実行時(より詳しく言えば、ラムダ式を含んだメソッドを初めて実行する際) に、メモリ上にクラスファイルが作られました。

メモリ上…、調べるのがとてもめんどくさいですね。

でも、簡単な方法があります。 システムプロパティ jdk.internal.lambda.dumpProxyClassesディレクトリを指定しておくと、その作られたクラスファイルを出力してくれるのです。

今回は、以下のように Java コマンドの引数でシステムプロパティを指定しました。

java -Djdk.internal.lambda.dumpProxyClasses=output Main

こうして実行すると、output ディレクトリにクラスファイルが出来上がりました。

  • Main$$Lambda$1.class

このクラスファイルを javap して、Javaソースコードに変換してみました。

Main$$Lambda$1.class の javap 結果

Classfile /C:/Users/YujiSoftware/Desktop/java/Main$$Lambda$1.class
  Last modified 2021/12/25; size 227 bytes
  SHA-256 checksum b1184aa81051852c052de612ab4b9f272f87cfd655717abb39dfa8ee74d8c5f4
final class Main$$Lambda$1 implements java.lang.Runnable
  minor version: 0
  major version: 59
  flags: (0x1030) ACC_FINAL, ACC_SUPER, ACC_SYNTHETIC
  this_class: #2                          // Main$$Lambda$1
  super_class: #4                         // java/lang/Object
  interfaces: 1, fields: 0, methods: 2, attributes: 0
{
  private Main$$Lambda$1();
    descriptor: ()V
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #10                 // Method java/lang/Object."<init>":()V
         4: return

  public void run();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #16                 // Method Main.lambda$main$0:()V
         3: return
}

final class Main$$Lambda$1 implements java.lang.Runnable {
  private Main$$Lambda$1() {
  }

  public void run() {
    Main.lambda$main$0();
  }
}

ラムダ式の中身である System.out.println("Hello world!"); がありません。 代わりに、 Main クラスの lambda$main$0() という static メソッドを呼び出しています。

このメソッドは何者でしょうか。 それを確認するために、Main.class を、 javap してみました。

すると、このクラス内に lambda$main$0() というメソッドが含まれていることが分かりました。

  private static void lambda$main$0();
    descriptor: ()V
    flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #21                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #27                 // String Hello world!
         5: invokevirtual #29                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 3: 0
private static void lambda$main$0() {
  System.out.println("Hello world!");
}

どういうことかというと…

  • コンパイル時に、 ラムダ式の中身が static メソッドに変換され、 コンパイルされる
  • 実行時に、 インタフェース(今回は Runnable )の実装クラスが作られ、 コンパイルされる
    • このクラスは、事前に作られた static メソッドを呼び出すだけ

匿名クラスとは、処理が大きく異なりますね。

外側のローカル変数を参照する場合

続いて、匿名クラスやラムダ式その外側にあるローカル変数を参照した場合、 どのようなクラスファイルが出来上がるのかを見ていきましょう。

おさらい:匿名クラスのコンパイル結果

今回は、 main メソッド内にあるmessage というローカル変数を匿名クラス内で参照した場合に、どのようなクラスファイルになるかを見ていきます。

public class Main {
  public static void main(String[] args) throws InterruptedException {
    var message = "Hello world!";
    var thread = new Thread(new Runnable() {
      @Override
      public void run() {
        System.out.println(message);
      }      
    });

    thread.run();
    thread.join();
  }
}

このクラスをコンパイルすると、先ほどと同様に2つのクラスファイルが出来上がりました。

  • Main.class
  • Main$1.class

後者のクラスファイルを、javap して、Javaソースコードに変換してみました。

Main$1.class の javap 結果

Classfile /C:/Users/YujiSoftware/Desktop/java/Main$1.class
  Last modified 2021/12/25; size 603 bytes
  SHA-256 checksum 4b78e2e34c543f25184362a99d8b506dbe2fd4c46e7a662e245e6b96b8908c95
  Compiled from "Main.java"
class Main$1 implements java.lang.Runnable
  minor version: 0
  major version: 61
  flags: (0x0020) ACC_SUPER
  this_class: #2                          // Main$1
  super_class: #8                         // java/lang/Object
  interfaces: 1, fields: 1, methods: 2, attributes: 4
{
  final java.lang.String val$message;
    descriptor: Ljava/lang/String;
    flags: (0x1010) ACC_FINAL, ACC_SYNTHETIC

  Main$1();
    descriptor: (Ljava/lang/String;)V
    flags: (0x0000)
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #1                  // Field val$message:Ljava/lang/String;
         5: aload_0
         6: invokespecial #7                  // Method java/lang/Object."<init>":()V
         9: return
      LineNumberTable:
        line 4: 0
    Signature: #12                          // ()V

  public void run();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: getfield      #1                  // Field val$message:Ljava/lang/String;
         7: invokevirtual #19                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        10: return
      LineNumberTable:
        line 7: 0
        line 8: 10
}
SourceFile: "Main.java"
EnclosingMethod: #34.#36                // Main.main
NestHost: class Main
InnerClasses:
  #2;                                     // class Main$1

class Main$1 implements java.lang.Runnable {
  final String val$message;

  Main$1(String arg1) {
    this.val$message = arg1;
    super();
  }

  public void run() {
    System.out.println(this.val$message);
  }
}

なんと、コンストラクタに引数が追加されています。 そして、その引数の値をフィールド変数として保持しています。

run() メソッド内では、そのフィールド変数の値を参照しています。 直接、ローカル変数の値を参照しているわけではないんですね…。

よく見ると、コンストラクタでフィールド変数に代入してから、super() を呼び出しています。 Java 言語仕様(8.8.7 コンストラクタの本体)としては、最初に this または super のコンストラクタを明示的または暗黙的に呼び出さなくてはいけないので、あれ?という感じがしますね。 でも、大丈夫です。この点は、Javaのクラスファイルとしては問題ありません。

:::note Java仮想マシン仕様:4.8.2 構造上の制約 】 クラス Object のコンストラクタから導出されたインスタンス初期化メソッドを除いた各インスタンス初期化メソッドは、該当インスタンス・メンバに対するアクセスの前に、this に対する別のインスタンス初期化メソッドか、その直接のスーパークラス super に対するインスタンス初期化メソッドのいずれかを呼び出さなければならない。しかし、インスタンス初期化メソッドの呼び出しに先立って、カレント・クラスで宣言されている this に対するインスタンスフィールドへの代入を行うことができる。 :::

ラムダ式コンパイル結果

続いて、ラムダ式の場合を見ていきましょう。 先ほどの匿名クラスを使って書いた部分を、ラムダ式に置き換えました。

public class Main {
  public static void main(String[] args) throws InterruptedException {
    var message = "Hello world!";
    var thread = new Thread(() -> System.out.println(message));

    thread.run();
    thread.join();
  }
}

このクラスをコンパイルすると、1つのクラスファイルが出来上がりました。

  • Main.class

そして、これをシステムプロパティ jdk.internal.lambda.dumpProxyClasses=output を付けて実行した結果、output ディレクトリにクラスファイルが出来上がりました。

  • Main$$Lambda$1.class

このクラスファイルを javap して、Javaソースコードに変換してみました。

Main$$Lambda$1.class の javap 結果

Classfile /C:/Users/YujiSoftware/Desktop/java/Main$$Lambda$1.class
  Last modified 2021/12/25; size 307 bytes
  SHA-256 checksum 9ba2c22eca9a152b636e2a0b00a4a12cb0039ed659914bcc48103ec6718a0d17
final class Main$$Lambda$1 implements java.lang.Runnable
  minor version: 0
  major version: 59
  flags: (0x1030) ACC_FINAL, ACC_SUPER, ACC_SYNTHETIC
  this_class: #2                          // Main$$Lambda$1
  super_class: #4                         // java/lang/Object
  interfaces: 1, fields: 1, methods: 2, attributes: 0
{
  private final java.lang.String arg$1;
    descriptor: Ljava/lang/String;
    flags: (0x0012) ACC_PRIVATE, ACC_FINAL

  private Main$$Lambda$1(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial #13                 // Method java/lang/Object."<init>":()V
         4: aload_0
         5: aload_1
         6: putfield      #15                 // Field arg$1:Ljava/lang/String;
         9: return

  public void run();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #15                 // Field arg$1:Ljava/lang/String;
         4: invokestatic  #21                 // Method Main.lambda$main$0:(Ljava/lang/String;)V
         7: return
}

final class Main$$Lambda$1 implements java.lang.Runnable {
  private final String arg$1;

  private Main$$Lambda$1(String arg1) {
    super();
    this.arg$1 = ar$1;
  }

  public void run() {
    Main.lambda$main$0(this.arg$1);
  }
}

匿名クラスと同様に、コンストラクタに引数が追加されています。 そして、その引数の値をフィールド変数として保持しています。

run() メソッド内では、そのフィールド変数の値を参照しています。 あとは、「外側のローカル変数を参照しない場合」と同じですね。

外側のローカル変数をいっぱい参照する場合

ローカル変数は、1メソッド内で 65,535 個まで宣言できます。 また、メソッドの引数には int 型などでは 255 個まで宣言できます。2 (コンストラクタも同様)

ということは、ラムダ式の内部から、外側のローカル変数を256個以上参照した場合はどうなるのでしょうか。 ここまで見てきたように、ローカル変数はコンストラクタの引数で受け渡していますが、メソッドの引数は 255 個までなので 256 個以上は渡せません。

ローカル変数を256個参照した場合

外側のローカル変数を256個参照するラムダ式を、実際にコンパイルしてみました。

public class Main {
  public static void main(String[] args) throws InterruptedException {
    var number1 = 1;
    var number2 = 2;
    (中略)
    var number255 = 255;
    var number256 = 256;

    var thread = new Thread(() -> {
      System.out.println(number1);
      System.out.println(number2);
      (中略)
      System.out.println(number255);
      System.out.println(number256);
    });

    thread.run();
    thread.join();
  }
}

その結果、 コンパイルエラーになりました。

C:\Users\YujiSoftware\Desktop\java>javac *.java 
Main.java:1: エラー: パラメータが多すぎます
エラー1個

特に対処することなく、256個の引数を持つ static メソッドを作ろうとしてエラーになるみたいです。

ちなみに、匿名クラスの場合はコンパイルに成功しました。 ただし、無効なクラスファイルになったため、実行時に ClassFormatError になりました。

C:\Users\YujiSoftware\Desktop\java>java -Djdk.internal.lambda.dumpProxyClasses=. Main 
Exception in thread "main" java.lang.ClassFormatError: Too many arguments in method signature in class file Main$1
        at java.base/java.lang.ClassLoader.defineClass1(Native Method)
        at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1012)
        at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150)
        at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:862)
        at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:760)
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:681)
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:639)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520)
        at Main.main(Main.java:260)

ローカル変数を255個参照した場合

ラムダ式で参照する外側のローカル変数を255個に減らしてみました。 すると、コンパイルに成功しました。

しかし、実行してみると BootstrapMethodError になりました。

C:\Users\YujiSoftware\Desktop\java>java -Djdk.internal.lambda.dumpProxyClasses=. Main 
Exception in thread "main" java.lang.BootstrapMethodError: java.lang.IllegalArgumentException: bad parameter count 256
        at Main.main(Main.java:260)
Caused by: java.lang.IllegalArgumentException: bad parameter count 256
        at java.base/java.lang.invoke.MethodHandleStatics.newIllegalArgumentException(MethodHandleStatics.java:167)
        at java.base/java.lang.invoke.MethodType.checkSlotCount(MethodType.java:223)
        at java.base/java.lang.invoke.MethodType.insertParameterTypes(MethodType.java:437)
        at java.base/java.lang.invoke.MethodType.appendParameterTypes(MethodType.java:461)
        at java.base/java.lang.invoke.DirectMethodHandle.makePreparedLambdaForm(DirectMethodHandle.java:256)
        at java.base/java.lang.invoke.DirectMethodHandle.preparedLambdaForm(DirectMethodHandle.java:233)
        at java.base/java.lang.invoke.DirectMethodHandle.preparedLambdaForm(DirectMethodHandle.java:218)
        at java.base/java.lang.invoke.DirectMethodHandle.preparedLambdaForm(DirectMethodHandle.java:227)
        at java.base/java.lang.invoke.DirectMethodHandle.make(DirectMethodHandle.java:108)
        at java.base/java.lang.invoke.MethodHandles$Lookup.getDirectMethodCommon(MethodHandles.java:4004)
        at java.base/java.lang.invoke.MethodHandles$Lookup.getDirectMethodNoSecurityManager(MethodHandles.java:3960)
        at java.base/java.lang.invoke.MethodHandles$Lookup.getDirectMethodForConstant(MethodHandles.java:4204)
        at java.base/java.lang.invoke.MethodHandles$Lookup.linkMethodHandleConstant(MethodHandles.java:4152)
        at java.base/java.lang.invoke.MethodHandleNatives.linkMethodHandleConstant(MethodHandleNatives.java:615)
        ... 1 more

java.lang.IllegalArgumentException: bad parameter count 256 とのことです。 これは、コンストラクタの最初の引数として暗黙的に自身のオブジェクトが追加されているため、引数の数が +1 されて256個になってしまったのが原因のようです。

ローカル変数を254個参照した場合

もう一個減らして、ラムダ式で参照する外側のローカル変数を254個にしてみました。

しかし、これも実行してみると BootstrapMethodError になりました。 (255個の時と微妙にスタックトレースが違います)

C:\Users\YujiSoftware\Desktop\java>java -Djdk.internal.lambda.dumpProxyClasses=. Main 
Exception in thread "main" java.lang.BootstrapMethodError: bootstrap method initialization exception
        at java.base/java.lang.invoke.BootstrapMethodInvoker.invoke(BootstrapMethodInvoker.java:188)
        at java.base/java.lang.invoke.CallSite.makeSite(CallSite.java:315)
        at java.base/java.lang.invoke.MethodHandleNatives.linkCallSiteImpl(MethodHandleNatives.java:281)
        at java.base/java.lang.invoke.MethodHandleNatives.linkCallSite(MethodHandleNatives.java:271)
        at Main.main(Main.java:259)
Caused by: java.lang.IllegalArgumentException: bad parameter count 256
        at java.base/java.lang.invoke.MethodHandleStatics.newIllegalArgumentException(MethodHandleStatics.java:167)
        at java.base/java.lang.invoke.MethodType.checkSlotCount(MethodType.java:223)
        at java.base/java.lang.invoke.MethodType.insertParameterTypes(MethodType.java:437)
        at java.base/java.lang.invoke.DirectMethodHandle.makePreparedLambdaForm(DirectMethodHandle.java:259)
        at java.base/java.lang.invoke.DirectMethodHandle.preparedLambdaForm(DirectMethodHandle.java:233)
        at java.base/java.lang.invoke.DirectMethodHandle.preparedLambdaForm(DirectMethodHandle.java:218)
        at java.base/java.lang.invoke.DirectMethodHandle.preparedLambdaForm(DirectMethodHandle.java:227)
        at java.base/java.lang.invoke.DirectMethodHandle.makeAllocator(DirectMethodHandle.java:142)
        at java.base/java.lang.invoke.DirectMethodHandle.make(DirectMethodHandle.java:133)
        at java.base/java.lang.invoke.MethodHandles$Lookup.getDirectConstructorCommon(MethodHandles.java:4122)
        at java.base/java.lang.invoke.MethodHandles$Lookup.getDirectConstructor(MethodHandles.java:4106)
        at java.base/java.lang.invoke.MethodHandles$Lookup.findConstructor(MethodHandles.java:2751)
        at java.base/java.lang.invoke.InnerClassLambdaMetafactory.buildCallSite(InnerClassLambdaMetafactory.java:269)
        at java.base/java.lang.invoke.LambdaMetafactory.metafactory(LambdaMetafactory.java:341)
        at java.base/java.lang.invoke.BootstrapMethodInvoker.invoke(BootstrapMethodInvoker.java:134)
        ... 4 more

これは、原因が分かりませんでした。 ラムダ式ではなく、匿名クラスだと参照するローカル変数が254個でも大丈夫なんですが…。

ちなみに、参照するローカル変数を253個にまで減らせば、ラムダ式でもエラーにならずに実行できました。

最後に

Java Advent Calendar 2021 も、これにて終了です!
みなさん、お疲れ様でした。

メリークリスマス!!!

SantaDuke.png


  1. これ以降も、同様に javap の結果を見ながら @YujiSoftware が気合で Javaソースコードに変換しているので、間違えているところがあるかも…。

  2. long や double は2個としてカウントします。「arg1 が long 型や double 型である場合、ローカル変数 1 と 2 が用いられる(Java仮想マシン仕様:6 Java仮想マシンの命令セット, invokespecial より)」