005 Java 版のビルド・テスト・静的解析・機械学習・API ライブラリの選定¶
「機械学習から始めるプログラミング入門」Java 版で使うライブラリを決める。
日付: 2026-09-19
ステータス¶
2026-09-19 提案されました
コンテキスト¶
執筆計画 の「Java 版執筆計画」で、Java 版は Kotlin 版の実装を対比の相手にし、データフレームのライブラリを使わず record のリストと Stream API でデータを表すことを承認した。JDK と Gradle の版は Kotlin 版にそろえる。
B24 のステップ 1 で、Maven Central の POM・JAR のクラスファイルの版と Gradle Plugin Portal のメタデータによって、次を確かめた。
| 確認したこと | 結果 | 確認方法 |
|---|---|---|
| ライセンス | Tribuo 4.3.2・AssertJ 3.27.7・Error Prone 2.50.0・google-java-format 1.36.1・Spotless 8.10.2・Javalin 7.2.3 は Apache License 2.0。JUnit 6.1.3・JaCoCo 0.8.15 は EPL-2.0。PMD 7.27.0 は BSD 系 | 各 POM の licenses |
| 必要な JDK | JUnit 6.1.3・Javalin 7.2.3 は Java 17(クラスファイルの major 61)、Error Prone 2.50.0・google-java-format 1.36.1 は Java 21(major 65)、Tribuo 4.3.2・PMD 7.27.0 は Java 8(major 52) | JAR のクラスファイルの版 |
| Gradle プラグイン | Error Prone は net.ltgt.errorprone 5.1.1、Spotless は com.diffplug.spotless 8.10.2。PMD と JaCoCo は Gradle に組み込み |
Gradle Plugin Portal |
| Tribuo の保守 | 最新は 4.3.2 で、Maven Central の最終更新は 2025-04-08 | Maven Central のメタデータ |
| AssertJ の版 | 4.0.0 はマイルストーン版(4.0.0-M1)のみ。安定版の最新は 3.27.7 | Maven Central のメタデータ |
| 静的解析が効いているか(B24) | わざと違反を入れた 3 つのファイルで、./gradlew check がそれぞれ失敗した。例外を作って投げない(Error Prone の DeadException、エラー)、整形の崩れ(spotlessJavaCheck)、波かっこの無い if(PMD の ControlStatementBraces)。Error Prone の警告(UnusedVariable・EmptyCatch)も -Werror によってコンパイルを失敗させる |
apps/java に一時的なファイルを置いて実行し、確かめた後に消した |
| 名前による除外(B24) | unused という名前の変数は、Error Prone も PMD も意図して使わない変数とみなし、指摘しない |
同上 |
| 名前付きのパッケージ(B24) | PMD の quickstart の NoPackage が、無名のパッケージのクラスを指摘する。テストも setup などのパッケージに置く |
同上 |
Nix の java 環境(B24) |
nix develop .#java の JDK 21.0.8 で ./gradlew test が成功した |
手元で実行 |
Tribuo の各アルゴリズムの振る舞いは、Kotlin 版の ADR 002 で確かめている。Tribuo は Java 製なので、Java から使っても振る舞いは変わらないと考え、置き換えの範囲は ADR 002 を起点にする。
決定¶
| 用途 | 採用 | バージョン | ライセンス | 初出 |
|---|---|---|---|---|
| 言語・ビルド | Java(ツールチェーン)、Gradle Wrapper(Kotlin DSL)、バージョンカタログ | 21、9.7.1 | — | 第 1 章 |
| テスト | JUnit、AssertJ | 6.1.3、3.27.7 | EPL-2.0、Apache License 2.0 | 第 1 章 |
| 整形 | Spotless(google-java-format) | 8.10.2(1.36.1) | Apache License 2.0 | 第 1 章(記事での解説は第 5 章) |
| 静的解析 | Error Prone、PMD | 2.50.0(プラグイン 5.1.1)、7.27.0 | Apache License 2.0、BSD 系 | 第 1 章(記事での解説は第 5 章) |
| カバレッジ | JaCoCo | 0.8.15 | EPL-2.0 | 第 1 章(記事での解説は第 5 章) |
| データの表現 | record のリストと Stream API(ライブラリなし) | — | — | 第 1 章 |
| 機械学習 | Tribuo | 4.3.2 | Apache License 2.0 | 第 3 章 |
| API | Javalin | 7.2.3 | Apache License 2.0 | 第 15 章 |
- JDK と Gradle は Kotlin 版と同じく、ツールチェーン(
java.toolchain)とgradle/gradle-daemon-jvm.propertiesで 21 に固定する。手元の JDK が 25 でも、ビルドとデーモンは JDK 21 で動く - 整形・静的解析・カバレッジは B24 から
./gradlew checkに組み込む。F# 版で静的解析のルールが 1 件も有効になっていなかった教訓から、わざと違反を入れてcheckが失敗することを確かめる - 機械学習と API のライブラリは、使う章に入ってから
gradle/libs.versions.tomlに追加する
章ごとのライブラリへの置き換え方針¶
ADR 002 の方針をそのまま使う。Java 版の各章で実装しながら振る舞いを確かめ、Kotlin 版と違う結果になったときは本 ADR に書き足す。
| 章 | 置き換え | 方針 |
|---|---|---|
| 3 | Tribuo の CART(ジニ不純度) | 自作の決定木と予測を突き合わせる |
| 7 | Tribuo の SLMTrainer(true)・LARSTrainer |
係数と決定係数を突き合わせる |
| 8 | Tribuo の決定木 | クラスの重み付けは自作の木で示し、Tribuo との突き合わせは重み付けなしで行う |
| 9 | Tribuo の MeanStdDevTransformation |
不偏標準偏差を使う違いを踏まえて自作の標準化と突き合わせる |
| 10 | Tribuo の LogisticRegressionTrainer・RandomForestTrainer |
正解率を比べる |
| 11 | Tribuo の評価器 | 自作の評価指標と突き合わせる |
| 12 | Tribuo の ElasticNetCDTrainer |
ラッソ回帰とリッジ回帰を突き合わせる |
| 13 | なし | PCA のモジュールが無いので、Tribuo の固有値分解を使った自作を最終実装とする |
| 14 | Tribuo の KMeansTrainer |
初期中心を渡せないので、SSE の大きさを比べるにとどめる |
各章で確かめた結果¶
| 章 | 確かめたこと |
|---|---|
| 2 | record の成分に配列を使うと、Error Prone の ArrayRecordComponent が警告する(-Werror でエラー)。特徴量の Features は double[] を持つので、record ではなくクラスにし、配列を写して持って equals・hashCode・toString を中身で比べる |
| 3 | Tribuo の CART(CARTClassificationTrainer、minChildWeight 1)と、同数の多数決・同じ不純度の分割候補の扱いが違うことは Kotlin 版(ADR 002)と同じ。Java 版の分割(java.util.Random(0))では、深さ 1〜5 と制限なしのどれでも、テストデータ 45 件の予測が自作と全件一致した |
| 4 | java.util.Random の数列は Javadoc の「Java implementations must use all the algorithms shown here」により仕様で保証されるが、Collections.shuffle の並べ方は実装の説明にとどまる。JDK 17・21・25 では Collections.shuffle(0..9, new Random(0)) が同じ並びになった。ツールチェーンを JDK 21 に固定して再現性を保つ |
| 5 | Gradle の依存のロック(dependencyLocking)を試すと、ロックファイルがバージョンカタログより優先され、カタログの版を下げても警告なくロックの版で解決した。範囲指定・動的な版が無いので、ロックは使わず、バージョンカタログの版を正とする。BOM の文字は Spotless・Error Prone・PMD・javac のどれも検出しないので、verifyNoBomCharacter で検査する |
| 7 | Java でも最小二乗解と一致するのは SLMTrainer(true) と LARSTrainer() だけ(実データの決定係数が自作と 1e-9 以内で一致)。SLMTrainer(false)・LinearSGDTrainer の予測は Kotlin 版と同じ値になる。SparseLinearModel の重みは正規化した空間の値で、元の単位に戻すと自作の係数と 1e-9 以内で一致する。Tribuo は特徴量を名前の順に並べるので、重みの位置は getFeatureIDMap().getID(name) で引く。RegressionEvaluator の MAE・RMSE・R² は自作と一致する |
| 8 | Tribuo の決定木にクラスの重みが無いので、自作の重み付きジニ不純度で示した。前処理後のテストデータ 179 件で、重み付けなしの自作の木と Tribuo の CART の予測が違ったのは深さ 5 で 2 件、深さ 9 で 1 件(ほかの深さは全件一致。原因は未調査)。toList() の結果の subList はシリアライズできない(NotSerializableException)ので、保存するモデルでは List.copyOf で写す。読み込みは ObjectInputFilter の許可リスト(chapter08.*;java.lang.*;java.util.*;!*)で絞る |
| 9 | Tribuo の MeanStdDevTransformation は Java から呼んでも不偏標準偏差(件数 − 1 で割る)を使い、自作の値に √((n−1)/n) を掛けると一致する(ADR 002 と同じ)。Shift_JIS の weather.csv を UTF-8 として読むと、Java の Files.readAllLines は MalformedInputException を投げる(Kotlin DataFrame は置換文字に置き換えて読み進める)。正規方程式は Tribuo の DenseMatrix のコレスキー分解で解き、choleskyFactorization() は Optional を返す |
| 10 | LinearSGDTrainer(ロジスティック回帰の既定の設定)は 5 エポックで訓練 0.9238・テスト 0.8889、50・500・5000 エポックでは自作と同じ 0.9143・0.9111 で落ち着く。RandomForestTrainer は minChildWeight の既定値 5 では訓練データを分け切らず(0.9905)、1 にすると分け切る。内側の決定木の fractionFeaturesInSplit が 1 だとコンストラクターで例外になる。RandomForestTrainer の tribuo-common-tree は tribuo-classification-tree から推移的に入る。PMD は型のパターンの使わない変数を UnusedLocalVariable で指摘するので、Java 21 では case Leaf ignored -> と書く |
| 11 | LabelEvaluator は正例を 1 件も予測しないと適合率・再現率・F 値を NaN ではなく 0.0 にし、混同行列の件数を double で返す。RegressionEvaluator に MSE は無いが、RMSE の 2 乗が自作の MSE と一致する。KFoldSplitter の余りの配り方は自作と一致し、1 回分の分割 TrainTestFold は train・test を public なフィールドで持つ。Error Prone は DoubleStream の戻り値を捨てると ReturnValueIgnored で止める |
| 12 | ElasticNetCDTrainer の l1Ratio は 0 と 1e-13 を拒否し(PropertyException)、1e-12 を受け付ける。alpha を件数で割って渡すと、自作のリッジ回帰と係数・切片が 1e-6 以内で一致する。係数は getWeights() で取り出し、特徴量の番号は getFeatureIDMap().get(name).getID() で引く |
| 13 | DenseMatrix.eigenDecomposition() は Java から呼んでも ADR 002 と同じく固有値を大きい順に返し、対称でない行列では空の Optional を返し、固有ベクトルの符号に規則は無い。分割も乱数も使わないので、寄与率・負荷量は Kotlin 版と一致した。PCA のモジュールが無いので自作を最終実装とした |
| 14 | KMeansTrainer は初期中心を受け取れず、同じシードなら SSE は Kotlin 版と一致する。KMeansModel.getCentroidVectors() は DenseVector[] の配列を返す。自作の K-means では、空のクラスタの中心が 0.0 / 0 で NaN になる問題をテストで見つけて直した |
| 15 | Javalin 7 の ctx.bodyAsClass は、JSON が読めないときや型が合わないときに BadRequestResponse に包まず、Jackson の例外(JsonEOFException・InvalidFormatException など)をそのまま投げる。BadRequestResponse にハンドラーを登録すると 500 になるので、JacksonException に登録して 422 にする。422 の定数名は HttpStatus.UNPROCESSABLE_CONTENT。PMD の AvoidUsingHardCodedIP を受けて、待ち受けるアドレスは InetAddress.getLoopbackAddress() から求める。Javalin 7.2.3 は Jackson 2 系(com.fasterxml.jackson)の JavalinJackson と Jackson 3 系の JavalinJackson3 の両方を持ち、Jackson は任意の依存なので明示して加える |
検討した代替案¶
| 代替案 | 採用しなかった理由 |
|---|---|
| Maven | Kotlin 版と同じ Gradle にそろえると、ビルドの設定の違いではなく言語の違いに記事を集中できる |
| JUnit 5 | JUnit 6 が安定版で、JDK 21 のツールチェーンで動く |
| AssertJ 4.0.0-M1 | マイルストーン版で、API が変わる可能性がある |
| Checkstyle | 整形は Spotless、規約の検査は PMD、バグになりやすい書き方は Error Prone で分担でき、規約の検査を 2 つ持つ必要が無い |
| Tablesaw(データフレーム) | Kotlin 版の data class と対比するため、record と Stream API で表す。依存を増やさない |
| Smile 6.x | GPL-3.0 で、PUBLIC リポジトリで配布するサンプルコードの利用条件に影響する(ADR 002 と同じ理由) |
| Spring Boot(API) | 第 15 章の主題(層の分離と統合テスト)に対して依存が大きい |
影響¶
- 良い影響: すべてのライブラリが寛容なライセンスで、サンプルコードの利用条件が単純になる
- 良い影響: Kotlin 版と同じ Tribuo・JDK・Gradle を使うので、記事の違いが言語の違いだけになる
- 悪い影響: Tribuo の最終更新は 2025-04 で、保守が続くかは分からない。Kotlin 版と Java 版の両方が影響を受ける
- 悪い影響: データフレームを使わないので、第 2 章・第 8 章・第 9 章の前処理は Kotlin 版より記述が長くなる。その長さ自体を対比の題材にする
コンプライアンス¶
apps/java/gradle/libs.versions.tomlに上記のライブラリと版だけが記載されている- Java CI(
.github/workflows/java-ci.yml)がグリーンである - わざと違反を入れると
./gradlew checkが失敗する(B24 で確かめた)
備考¶
- 著者: claude-code/claude-opus-5