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
- A: 非Javaエンジニアが「プロになるJava」で再勉強した話 / 白栁隆司
- A: Git 未経験者が GitHub Actions で CI/CD できるようになるまで / 浅野正貴
- B: Javaで実現するフォールトトレランス 〜Resilience4jを使ったサーキットブレーカーの実装〜 / 今野裕介
- B: AWS環境におけるSpring BootアプリケーションのCI/CDをCircleCIで構築した話 / 篠原 正太
- C: バーチャルスレッド詳細 / もちだ
- D: MicroProfile JWTを使って、マイクロサービスをセキュアにしよう / 高宮 裕子
11:00
- A: LINE NEWSにおけるJava移行の5年間の歩みとこれから / 森藤 賢司
- B: ミリ秒で起動するFullplatformのアプリケーションサーバー・Liberty InstantOnでクラウドネイティブにひろがるJakarta EEの世界 / 田中 孝清
- C: Build OpenJDK をコンテナ環境で利用する際のベスト・プラクティス(
動画) / てらだ よしお
- D: カード決済基幹システム レガシーの克服と無停止更改の挑戦 / 羽鳥樹
12:30
- A: 入門:テスト技法とJUnit / 多田真敏
- B: Spring BootとKubernetesで実現する今どきのDevOps入門 / 佐藤靖幸
- C: コンテナ環境でのJava技術の進化 / 数村憲治
- D: JJUG CCC 2022 Fall: JJUG初心者のための Javaコミュニティのススメ / JJUG
13:30
- A: 未来を見据えた CI/CD ~ 10年後も使える ビルド・テスト パイプライン ~ / 野中 翔太
- B: gaugeで学ぶ実行可能ドキュメントの価値 / 清家蒼一朗
- C: ユーザー数100万人規模の事業成長を止めずに、レガシーコードと戦う / 菊池 信太郎
- D: Java開発ツールのあれこれ ~便利そうなツールを色々集めてみた~ / 大城 夏樹
15:00
- A: Javaの入門が終わったら何の勉強をすればいいの? / きしだ なおき
- B: 5年ぶりのメジャーアップデート! Spring Framework 6 & Spring Boot 3 / 槙 俊明
- C: FizzBuzzで学ぶJava 7以降のJavaの進化 / 河野裕隆
- C: Fargate上のJVMからCPUを認識するまで 〜正しく認識されないCPUの謎を追え〜 / orekyuu
- D: 脆弱性対応を支える技術 / 梶紳之介
- D: 非同期メッセージングサービスを使ったLINEメッセージ配信の改善 / 平井 一史
16:00
- A: 組織と技術の両輪で開発を加速させるkintoneチームの取り組み / 濵田 健
- B: Jaegerによる分散トレーシングの実践 ~マイクロサービスのボトルネック解消~ / 内田 理絵
- C: Red Hat Application Foundationsから学ぶアーキテクチャー入門 / 瀬戸 智
- D: クラウド時代のデータアクセス仮想化のススメ / 疋田圭介
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
Jextract を使って、Java から libcurl を使ってみる
Jextract に libcurl を呼び出すサンプルがありました。これを試しに動かしてみました。 (まだ Preview の Foreign 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 に基づいて構築されます。
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 によって管理されないメモリ) に安全にアクセスすることにより、API は Java プログラムがネイティブ ライブラリを呼び出し、ネイティブ データをもろくて危険なJNIを使わずに処理できるようにします。 これはプレビュー API です。
また、先日の 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
で必要なものをインストールします。
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 の 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 の有無
名称 | 読み込み時 バイトオーダー |
書き込み時 バイトオーダー |
書き込み時 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 |
年/月/日 の一部が省略された日付をパースして 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
- A: JUnitで闘うレガシーコード改善(
動画) / 齋藤 悠太
- A: Testcontainersでコンテナを使ったテストを実行しよう / なかやまひろ(せち)
- B: AWS Batch × Spring Batch でクラウド最適なバッチを構築した話(
動画) / 猪熊 朔也
- B: Spring Boot と WebRTC を用いた Web 会議システムの開発(
動画) / 源 拓洋
- C: 開発者にやさしく、柔軟性、安全性を高めたGithub ActionsベースのCI/CDを構築する(
動画) / 大橋政也
- C: Java で作るカスタム GitHub Actions(
動画) / Tetsuya Morimoto
- D: サービス開発の理想と現実・短納期でローンチした新サービスをJavaで開発した話(
動画) / a1
11:00
- A: クラウドネイティブ環境におけるJavaチューニングの進め方 - 超PayPay祭の事例(
動画) / 川口 貴之
- B: クレジットカード決済システムをJavaで構築して10年間運用した話(
動画) / 米原 孝太
- C: k8s 疲れの方へ送る、k8s ベースのらくらくマイクロサービス動作基盤のご紹介〜 Dapr ベースのマイクロサービス開発から GitHub Action を利用した CI/CD 〜(
動画) / てらだ よしお
- D: 「サポート」は製品開発? - JDBCライブラリ屋さんが実践する攻めのテクニカルサポートとJavaエンジニアのキャリアについて -(
動画) / 杉本 和也
12:30
- A: JDK Flight Recorder入門(
動画) / 伊藤ちひろ
- B: ライブコーディングで学ぶKotlinチュートリアル(
動画) / 小山 健太
- B: RabbitMQを用いたイベント駆動アーキテクチャの紹介(
動画) / Seiki Tokunaga
- C: Java初心者が知っておくべきプログラミングのこと(
動画) / きしだ なおき
- D: Lauchableで僕が学んだ働き方 〜リモートワークで会社もプロダクトも1から作る経験〜(
動画) / Yoshiori Shoji
13:30
- A: Java アプリとAWS の良い関係 - AWS でJava アプリを実行する一番簡単な方法教えます(
動画) / 宇賀神 みずき, 下川 賢介, 金森 政雄 (アマゾン ウェブ サービス ジャパン合同会社 ソリューションアーキテクト)
- B: 分散データベースTiDB Cloudで構築するWebアプリケーション(
動画) / 本多康夫
- C: Azure Pipelinesを使って学んだ CI/CD環境のあれこれ(
動画) / 大城 夏樹
- D: 保険基幹システムのアーキテクチャ : シンプレクスの技術的チャレンジ(
動画) / 白井 翔太
15:00
- A: パターンマッチングを学んで新しいJavaの世界へ!Java 18までの目玉機能をおさらいしよう(
動画) / よこな
- B: JUnit5.7, 5.8の新機能紹介(
動画) / きょん
- C: RDRA + JavaによるレジャーSaaSプロダクトの要件定義と実装のシームレスな接続(
動画) / 江部隼矢
- D: 脱二重メンテナンス!ドキュメント自動生成への道(
動画) / 進藤 遼
- D: Javaのビルドやバージョンの違いをグラフデータベースで理解する。移行で困らないための知識グラフを作ろう。(
動画) / Koji Annoura
16:00
- A: Javaの認証認可基盤のKeycloak(Red Hat Single Sign-on)(
動画) / 井上 貴博
- B: LINEのB2Bプラットフォームにおけるトラブルシューティング2選(
動画) / 長谷部 良輔
- C: 連続画像処理による位置情報計算を支えるマイクロサービスアーキテクチャ(
動画) / 本山 要
- D: スナップショットログを用いた自動E2Eテストの導入(
動画) / 濱岡 佑介
17:00
- A: バイトコードって言葉をよく目にするけど一体何なんだろう?(
動画) / 阪田 浩一
- B: テストコードの注入から始めるレガシーコードのリファクタリング(
動画) / 風間裕也(ブロッコリー)
- C: イベントソーシング入門 in Java――イベントストーミングから Akka persistence を使った CQRS+ES 実装まで(
動画) / 成瀬 允宣
過去記事
- JJUG CCC 2022 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2021 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2020 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2019 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2019 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2018 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2018 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2017 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2017 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2016 Fall ( #jjug_ccc ) - セッション資料の一覧へのリンク
- JJUG CCC 2016 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2015 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2015 Spring ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2014 Fall ( #jjug_ccc ) - セッション資料の一覧
- JJUG CCC 2014 Spring ( #jjug_ccc ) - セッション資料の一覧
-
発表者のお名前は敬称略とさせていただきました。↩
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 も、これにて終了です!
みなさん、お疲れ様でした。
メリークリスマス!!!