Java
目次
主要項目のみを表示しています。詳細な小見出しは本文内で確認できます。
- 概要
- 1. Javaとは何か
- 2. 実行環境と動作の仕組み
- 6. クラスとオブジェクト
- 7. 継承・多態性・interface
- 8. ジェネリクス
- 9. コレクションフレームワーク
- 10. 関数型インターフェースとラムダ
- 11. Streams API
- 12. Optionalとnull安全
- 13. recordとsealed(モダンJavaの表現力)
- 14. 例外処理
- 15. 並行・並列処理
- 16. モジュールシステム(Project Jigsaw)
- 17. ビルドと依存管理(Maven / Gradle)
- 18. テスト戦略
- 19. パフォーマンス
- 20. Java 8〜21の進化
- 21. よくある落とし穴FAQ
- 22. 図解: メモリモデル / クラスローディング / GC
- 23. 学習ロードマップ(30日)
- 24. 用語集
- 発展: JVMと実務機能
- 25. JVM内部詳細
- 26. Streams API完全版
- 27. CompletableFuture深掘り
- 28. Virtual Threads(Java 21+)詳細
- 29. Spring Boot入門
- 30. MavenとGradle詳細
- 応用: Springと運用
- JVM仕様の詳細とバージョン履歴
- メモリ管理とガベッジコレクション(GC)
- マルチスレッドと同期化
- ストリームAPIと関数型プログラミング
- まとめ
- 参考文献
概要
まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。
コード例は、そのまま写すためだけのものではありません。直前の本文で「何を確かめる例か」を押さえ、直後の説明で「どの性質が見えるか」を確認してください。実務では、ここに入力の境界、失敗時の挙動、依存する実行環境を足して読むと判断しやすくなります。
Javaは、JVMの上で移植性、静的型付け、豊富な標準ライブラリとエコシステムを提供する言語です。
このページでは、JVM、型、オブジェクト指向、ジェネリクス、コレクション、Stream、並行処理、近年の言語機能を整理します。
1. Javaとは何か
このセクションでは「Javaはなぜ生まれたのか」「なぜ静的型付けでJVM上で動くのか」「30年経った今もなぜ使われ続けるのか」を解説します。Javaは産業界で最も成熟したエコシステムを持つ言語のひとつで、その背景を理解するとライブラリやフレームワークの設計判断が見えてきます。
Javaは、**「静的型付け・クラスベースオブジェクト指向・JVM上で動く」**プログラミング言語です。1995年にSun Microsystemsから公開され、現在は次のような領域で広く使われています。
- エンタープライズバックエンド: Spring Framework、Jakarta EE、マイクロサービス
- Androidアプリ: Android SDKの中核(近年はKotlinと併用)
- ビッグデータ: Hadoop、Spark、Kafka、Elasticsearch
- 金融・保険: 大規模ミッションクリティカルシステム
- 科学技術計算: 天文・物理シミュレーション
- JVM言語のホスト: Kotlin、Scala、Clojure、Groovy
1-1. Javaの歴史(OakからJava 21まで)
「Oak」と家電の野望
1991年、Sun Microsystemsの James Gosling が中心となって、家電製品(テレビのセットトップボックスなど)向けの新しい言語の開発が始まりました。当初の名前は 「Oak」。Goslingのオフィスの窓から見えるオークの木にちなんだ命名でした。
1994年にWebブラウザが急速に普及すると、開発チームは方向転換し、「ブラウザの中で動くプログラム言語」 としてOakを作り直します。1995年、商標問題で名前を 「Java」 に変更(コーヒーの “Java” が由来)し、Sun Microsystemsから公開されました。
AppletとWebの黎明期(1995〜2000)
Javaの最初の躍進は Java Applet(ブラウザ内で動く小プログラム)でした。当時のWebは静的なHTMLが中心で、動的な要素はCGIとフォームしかなかった時代に、「ブラウザで動くアプリ」 は革命的でした。
1995 Java 1.0公開
1997 Java 1.1(内部クラス、JDBC、JavaBeans)
1998 Java 2(J2SE 1.2)「Java 2 Platform」、Swing
2000 J2SE 1.3(HotSpot標準化)
しかしAppletは 重い起動時間・セキュリティ問題 で次第に廃れ、Javaの主戦場は サーバサイド に移っていきます。
エンタープライズJavaの確立(2000〜2010)
2000年代、Javaはエンタープライズシステムの王者になりました。
- J2EE(後のJakarta EE): サーバサイド標準
- Spring Framework(2003〜): 軽量なJavaフレームワーク
- Hibernate: ORM
- Eclipse / IntelliJ IDEA: 強力なIDE
2004 Java 5(J2SE 1.5)ジェネリクス、アノテーション、拡張for、列挙型
2006 Java 6(性能改善)
2011 Java 7(try-with-resources、ダイヤモンド演算子)
特に Java 5 は歴史的に重要で、ジェネリクスやアノテーションといった現代Javaの基礎が一気に整いました。
Oracle買収とJava 8の衝撃(2010〜2014)
2010年、Sun MicrosystemsをOracleが買収し、Javaの管理がOracleに移ります。コミュニティから不安の声もあった中、Java 8(2014) は近代化の決定打になりました。
- ラムダ式・関数型インターフェース
- Streams API
- Optional
- デフォルトメソッド
- 新しい日時API(java.time)
それまで「冗長で時代遅れ」と批判されることもあったJavaが、関数型プログラミングの要素を取り込み、モダン化しました。Java 8は今でも企業で広く使われているLTS です。
LTS体制とリリース加速(2017〜)
2017年、Oracleは 6か月ごとの定期リリース + 特定バージョンをLTS(長期サポート) という新体制に移行。これにより新機能が頻繁に試せるようになりました。
2017 Java 9モジュールシステム(Project Jigsaw)
2018 Java 10 varによる型推論
2018 Java 11 LTS、HTTP Client、Files API強化
2019 Java 12〜13 switch式(プレビュー)
2020 Java 14〜15 record(プレビュー)、sealed(プレビュー)
2021 Java 17 LTS、record・sealed・パターンマッチング
2022 Java 18〜19 Virtual Threads(プレビュー)
2023 Java 21 LTS、Virtual Threads・パターンマッチングfor switch・Sequenced Collections
2024 Java 22〜23ストリームのギャザラー、ステートメントの前のスーパー
Java 21 LTS(2023)の重要性
2023年9月リリースの Java 21 は、Java 17以降の重要機能をまとめた現代的なLTSです。
- Virtual Threads(Project Loom): 軽量スレッドでI/O並行が劇的に楽に
- パターンマッチングfor switch: 構造的パターンマッチング
- Sequenced Collections: 順序付きコレクションの統一インターフェース
- recordとsealed: データクラスと封印型
「Javaは変わらない」という古いイメージとは裏腹に、近年のJavaは モダン化が著しく進んでいるのが現実です。
1-2. なぜ静的型付けか
Javaは 静的型付け 言語です。すべての変数・引数・戻り値はコンパイル時に型が決まり、コンパイラが型エラーをチェックします。
int x = 42;
x = "hello"; // コンパイルエラー! intにStringは代入できない
Javaが静的型付けを選んだ理由
- 大規模システム向けの設計: SunはJavaを 「企業の基幹システムを書く言語」 として位置付けた。型エラーをコンパイル時に発見できる安全性が必須
- C++ の影響: GoslingはC++ を強く意識してJavaを設計。静的型はC++ の常識
- JVM上での最適化: 型情報があるとJITが効率的なコードを生成できる
- IDE支援: 補完・リファクタリング・参照検索などが強力に
静的型付けのメリット・デメリット
| 観点 | 静的型付け(Java) | 動的型付け(Python/Ruby) |
|---|---|---|
| 型エラー発見 | コンパイル時 | 実行時 |
| IDE支援 | 強力 | 限定的 |
| 学習コスト | 高い | 低い |
| 実行速度 | 速い | 遅い |
| プロトタイプ速度 | やや遅い | 速い |
| 大規模開発 | 向いている | 困難 |
近年の進化: varによる型推論
Java 10で var による型推論が導入されました。型を書く手間を減らしつつ、静的型の安全性は保つ仕組みです。
// Java 9まで
List<Map<String, Integer>> map = new ArrayList<>();
// Java 10+
var map = new ArrayList<Map<String, Integer>>();
これにより、「冗長な型宣言」というJavaの代表的な不満が大きく改善されました。
1-3. Write Once, Run Anywhereの哲学
Javaの最大の特徴は 「Write Once, Run Anywhere(WORA)」。一度コンパイルしたバイトコードは、JVMが動く環境ならどこでも動く。
ソース (.java) → バイトコード (.class) → JVM → 各OSで実行
(プラットフォーム非依存)
Sunはこれを「家電からサーバまで、すべてのデバイスで動く言語」というビジョンで打ち出しました。実際、Javaはサーバ・デスクトップ・モバイル・組み込み・スマートカードまで広範に展開されました。
JVMの威力
JVMは OSとCPU命令セットを抽象化します。同じ .class ファイルがLinux/Mac/Windows、x86/ARM/SPARCで動きます。これはC/C++ では実現が大変なことです。
WORAの現代的意義
クラウド・コンテナ時代になり、「OSの差を気にしない」というWORAの価値は別の意味で重要に。DockerイメージにJDKを入れて使うのが標準的になりました。
1-4. JavaとJVM言語の関係
JVMはJava専用ではなく、多くの言語のホストとして機能しています。
Java ─┐
Kotlin ─┤
Scala ─┼─→ JVMバイトコード → JVM実行
Clojure ─┤
Groovy ─┘
| 言語 | 特徴 |
|---|---|
| Kotlin | GoogleがAndroid公式に採用。Javaと完全互換、簡潔 |
| Scala | 関数型 + OOP。Sparkの中核 |
| Clojure | LISP系。データ処理・並行処理が得意 |
| Groovy | スクリプト的、Gradleのスクリプト言語 |
これらの言語は Javaの標準ライブラリと相互運用可能で、巨大なJavaエコシステムを共有できます。「Javaを選ぶ = JVMとエコシステムを選ぶ」と言ってもよく、新規プロジェクトではKotlinを選んでもJavaの知識は無駄になりません。
1-5. このセクションのまとめ
Javaの出自:
1991年Sun Microsystemsで「Oak」として開始
1995年Java 1.0公開、当初はAppletが花形
2000年代以降、エンタープライズサーバの王者に
2010年Oracle買収、6ヶ月リリース体制に
2023年Java 21 LTS(Virtual Threads・パターンマッチング)
哲学:
Write Once, Run Anywhere(WORA)
静的型付けで大規模・安全
JITによる高速実行
JVM言語:
Kotlin / Scala / Clojure / Groovyも同じバイトコードへ
Javaの標準ライブラリ・エコシステムを共有
近年の進化:
Java 8で関数型導入(ラムダ・Streams)
Java 17/21でモダン化(record・sealed・パターンマッチング・Virtual Threads)
「Javaは古い」というイメージは現代では誤り
次のセクションでは、Javaの実行環境――JDK / JRE / JVM、JIT、GCを扱います。
2. 実行環境と動作の仕組み
このセクションでは「javac Hello.java && java Hello で何が起こっているのか」を解説します。JVM・バイトコード・JIT・クラスローダー・GCはJavaを理解する上で必須の前提知識です。
2-1. JDK / JRE / JVMの違い
Javaの世界には3つの似た略語があります。
JVM (Java Virtual Machine) バイトコードを実行する仮想マシン
JRE (Java Runtime Env) JVM + 標準ライブラリ(実行に必要なもの)
JDK (Java Development Kit) JRE + コンパイラ等の開発ツール
JDK
├── javac(コンパイラ)
├── jar(パッケージング)
├── jdb(デバッガ)
├── javadoc(ドキュメント生成)
└── JRE
├── JVM
└── 標準ライブラリ(java.lang, java.util, ...)
開発時はJDKを入れる
開発するならJDK一択。Oracle JDK / OpenJDK / Adoptium / Amazon Correttoなどのディストリビューションがあります。
# 主要なディストリビューション
- OpenJDK上流(リファレンス実装)
- Oracle JDK Oracle製、商用
- Adoptium / Eclipse Temurinコミュニティ標準
- Amazon Corretto AWS製、無料・無期限サポート
- Azul Zulu Azul製
- GraalVM Oracle製、AOTコンパイル可能
実用上は Adoptium(Eclipse Temurin) または Amazon Corretto が無料で安定。
Java 17/21 LTS
LTS(Long-Term Support)バージョンは長期サポートが受けられるため、本番環境ではこれらを選ぶのが原則。Java 17(2021) か Java 21(2023) が現代の主要LTS。
2-2. バイトコードと .classファイル
Javaのソース .java を javac でコンパイルすると、バイトコードを含む .class ファイルになります。
javac Hello.java # → Hello.class
java Hello # JVMが .classを実行
バイトコードを覗く
javap -c でバイトコードを逆アセンブルできます。
public class Hello {
public static void main(String[] args) {
System.out.println(1 + 2);
}
}
$ javap -c Hello
Compiled from "Hello.java"
public class Hello {
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out
3: iconst_3 // 1+2はコンパイル時に計算済み
4: invokevirtual #3 // Method java/io/PrintStream.println
7: return
}
JVMは スタックマシン として動作します。各命令はオペランドスタックを操作する形式で、これはSmalltalkからPython・Rubyまで多くの言語で使われている古典的設計です。
2-3. JITコンパイルとHotSpot
JVMは最初バイトコードを インタプリタとして実行しますが、頻繁に呼ばれるコード(ホットスポット) を実行時に機械語にコンパイルします。これが JIT(Just-In-Time) コンパイル。
1. 起動時はバイトコードをインタプリタ実行(速い起動)
2. 実行回数のしきい値を超えたメソッドをJIT機械語化
3. プロファイル情報を元に最適化(インライン化、ループ展開など)
4. Cで書かれたネイティブコードに匹敵する速度を達成
HotSpot JVM
OpenJDK / Oracle JDKの標準JVMが HotSpot。1999年にリリースされ、現在まで進化を続けています。
C1とC2コンパイラ
HotSpotは2段階のJITを持ちます。
C1 (Client Compiler): 軽量、起動を速くする最適化
C2 (Server Compiler): 重量、ピーク性能のための最適化
-XX:+TieredCompilation(デフォルト)で両者を協調させます。
GraalVM
Oracle Labsが開発する 次世代JIT/AOT プラットフォーム。
- Graal JIT: Javaで書かれたJIT。HotSpotより柔軟で高速な場合がある
- GraalVM Native Image: AOTコンパイルで起動瞬時のネイティブ実行可能ファイルを生成(マイクロサービス・サーバレス向け)
2-4. クラスローダー
Javaの クラスローダー は .class ファイルをJVMにロードする仕組み。Javaの動的性の核。
Bootstrap ClassLoader JVM自身、java.lang.* 等
↓
Platform ClassLoaderプラットフォーム標準ライブラリ
↓
Application ClassLoaderユーザコード(CLASSPATH上のクラス)
各クラスローダーは 親に問い合わせてから自分が探すという委譲モデル(Parent Delegation Model)に従います。これにより java.lang.String を上書きしようとしても安全です。
クラスロードのタイミング
クラスは 実際に使われるときにロードされます(lazy loading)。new MyClass() または MyClass.STATIC_FIELD で初めてロード・初期化が走ります。
モジュールシステム(Java 9+)
Java 9で モジュール(Project Jigsaw) が導入され、クラスローディングはさらに洗練されました。詳細は第16章。
2-5. ガベージコレクション(G1/ZGC/Shenandoah)
Javaは 自動メモリ管理(GC) を持つ言語。プログラマがメモリを明示的に解放する必要がなく、不要になったオブジェクトはGCが回収します。
世代別GC
JVMの標準的なGCモデルは 世代別仮説に基づきます。
仮説: ほとんどのオブジェクトはすぐに死ぬ
一部のオブジェクトは長く生きる
→ 新しいオブジェクトをYoung世代に置き、頻繁にGC
→ 生き残ったものをOld世代に昇格、たまにGC
ヒープ
├── Young Generation
│ ├── Eden (新規生成)
│ ├── Survivor 0
│ └── Survivor 1
└── Old Generation (長生きするオブジェクト)
主要なGCアルゴリズム
| GC | 特徴 |
|---|---|
| Serial GC | 単一スレッド、小規模アプリ向け |
| Parallel GC | スループット重視、複数スレッドでGC |
| G1 (Garbage First) | ヒープを領域分割、低レイテンシ重視(Java 9+ デフォルト) |
| ZGC | 超低レイテンシ(停止1ms以下)、巨大ヒープ対応 |
| Shenandoah | ZGCと同様、Red Hat開発 |
# GCを選択
java -XX:+UseG1GC ...
java -XX:+UseZGC ...
java -XX:+UseShenandoahGC ...
Generational ZGC(Java 21)
Java 21で 世代別ZGC が登場し、ZGCが世代別最適化と低レイテンシを両立しました。巨大ヒープ + 低レイテンシが必要な現代のサービス(金融・ゲーム)で重要。
GCのチューニング
java -Xms512m -Xmx4g # 最小・最大ヒープ
java -XX:MaxGCPauseMillis=200 # 目標停止時間
java -Xlog:gc* # GCログを詳細出力
「まずはG1で動かして、ログを見てから検討」が現代のセオリー。
2-6. このセクションのまとめ
JDK / JRE / JVM:
JVMバイトコード実行
JRE JVM + 標準ライブラリ
JDK JRE + コンパイラなど開発ツール
実行モデル:
.java → javac → .class(バイトコード)→ JVM
HotSpotがインタプリタ + JITで実行
C1(起動速度)とC2(ピーク性能)の階層化
クラスローダー:
Bootstrap → Platform → Applicationの委譲モデル
実際に使われるときにロード(lazy)
GC:
世代別(Young / Old)が基本
G1(Java 9+ デフォルト)、ZGC・Shenandoahが低レイテンシ
Java 21でGenerational ZGCが登場
GraalVM:
Java製の次世代JIT
Native ImageでAOT起動の瞬時化(マイクロサービス向け)
次のセクションでは、Javaの型システム――プリミティブvs参照、null、== と equals の違いを扱います。
6. クラスとオブジェクト
JavaのOOPモデルは 「クラスベース・単一継承・複数interface実装」。後の言語(C#、Kotlin、Scala)に大きな影響を与えた古典的な設計です。
6-1. クラス定義とthis
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name; // thisでフィールドを区別
this.age = age;
}
public String greet() {
return "Hi, I'm " + this.name;
}
}
Person p = new Person("Alice", 30);
p.greet(); // "Hi, I'm Alice"
this は 「現在のインスタンス自身」。コンストラクタやsetterで引数とフィールド名がかぶるとき必須。
6-2. アクセス修飾子
| 修飾子 | クラス内 | 同一パッケージ | サブクラス | どこからでも |
|---|---|---|---|---|
private |
○ | × | × | × |
| (なし、package-private) | ○ | ○ | × | × |
protected |
○ | ○ | ○ | × |
public |
○ | ○ | ○ | ○ |
慣習
- フィールドは原則private(カプセル化)
- getter/setterで公開
- APIとして外部に公開するクラスはpublic
- 内部実装はpackage-private(テストから見えるくらい)
public class User {
private String name; // 内部状態は隠す
public String getName() { return name; } // 公開
public void setName(String name) { this.name = name; }
}
6-3. コンストラクタ・this()・super()
コンストラクタの基本
public class Point {
private int x, y;
public Point() {
this(0, 0); // 同じクラスの別コンストラクタを呼ぶ
}
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
this(...) は コンストラクタの最初の文 でしか書けない。
super()
サブクラスから親クラスのコンストラクタを呼ぶ。
public class Animal {
private String name;
public Animal(String name) { this.name = name; }
}
public class Dog extends Animal {
private String breed;
public Dog(String name, String breed) {
super(name); // 親のコンストラクタを呼ぶ
this.breed = breed;
}
}
super() は コンストラクタの最初の文。書かないとコンパイラが暗黙的に super()(引数なし)を挿入。親に引数なしコンストラクタが無いとエラー。
6-4. static
static 修飾子は 「クラスに属する」 ことを示す。インスタンス化せずに使える。
public class MathUtils {
public static final double PI = 3.14159;
public static int square(int x) {
return x * x;
}
}
MathUtils.PI; // インスタンス不要
MathUtils.square(5);
staticフィールド・メソッド
public class Counter {
private static int total = 0; // クラス全体で共有
private int value;
public Counter() {
total++;
value = total;
}
public static int getTotal() { return total; }
}
staticイニシャライザ
クラス読み込み時に1回だけ実行。
public class Config {
private static final Map<String, String> DEFAULTS;
static {
DEFAULTS = new HashMap<>();
DEFAULTS.put("host", "localhost");
DEFAULTS.put("port", "8080");
}
}
staticの落とし穴
- テストしにくい(モック化が困難)
- 状態を持つstaticは並行性が悪い
- オーバーライドできない(隠蔽はできる)
「ユーティリティ関数」「定数」以外は安易にstaticを使わないのが現代の作法。
6-5. final
final は 「変更できない」 を意味する。
final変数
final int MAX = 100;
MAX = 200; // コンパイルエラー
finalフィールド(イミュータブル設計)
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
// setterなし
}
これで イミュータブル なクラスが作れる。マルチスレッドで安全。
finalメソッド
public class A {
public final void method() { ... } // オーバーライド禁止
}
finalクラス
public final class String { ... } // 継承禁止(標準のStringもこれ)
String、Integer などの基本クラスはすべて final。継承による誤動作を防ぐためです。
6-6. equals / hashCode / toString
すべてのJavaクラスは Object を継承し、これらのメソッドを持ちます。equals をオーバーライドしたら hashCode もペアでが鉄則。
equalsの契約
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
5つの契約:
- 反射律:
x.equals(x)は常にtrue - 対称律:
x.equals(y)⇔y.equals(x) - 推移律:
x.equals(y)&&y.equals(z)⇒x.equals(z) - 一貫性: 何度呼んでも同じ結果
- nullとの比較:
x.equals(null)はfalse
hashCodeの契約
@Override
public int hashCode() {
return Objects.hash(x, y);
}
x.equals(y)ならx.hashCode() == y.hashCode()
これが守られないと HashMap / HashSet が壊れる。
toString
@Override
public String toString() {
return "Point(" + x + ", " + y + ")";
}
デバッグ・ログで重要。println(obj) で自動的に呼ばれる。
recordで自動生成(Java 14+)
public record Point(int x, int y) {}
// equals, hashCode, toStringが自動生成
これが現代の解決策。第13章で詳述。
6-7. このセクションのまとめ
クラス定義:
class Foo { ... }
thisでインスタンス自身を参照
コンストラクタはthis() / super() で連鎖
アクセス修飾子:
private(クラス内)/ なし(パッケージ)/ protected / public
原則: フィールドはprivate、APIはpublic
コンストラクタ:
this(...) で同クラスの別コンストラクタ呼び出し
super(...) で親コンストラクタ呼び出し(最初の文)
static:
クラスに属する、インスタンス不要
ユーティリティと定数以外は控えめに
final:
変数: 再代入禁止
フィールド: イミュータブル設計
メソッド: オーバーライド禁止
クラス: 継承禁止
equals / hashCode / toString:
値クラスでは必ずオーバーライド
equals/hashCodeは契約を守る(ペアで)
→ recordで自動生成(Java 14+)
次のセクションでは、Javaの継承・多態性・interface・sealedクラスを扱います。
7. 継承・多態性・interface
JavaのOOPの核である継承とinterface。「単一継承 + 複数interface実装」 というJavaの選択は、C++ の多重継承の問題を回避した現代OOPの標準モデルになりました。
7-1. 単一継承(extends)
public class Animal {
public void speak() { System.out.println("some sound"); }
}
public class Dog extends Animal {
@Override
public void speak() { System.out.println("Woof!"); }
}
Animal a = new Dog();
a.speak(); // "Woof!"(多態性: 実際の型のメソッドが呼ばれる)
Javaは 単一継承。extends で1つの親クラスだけ指定可能。
@Overrideアノテーション
@Override を付けると 「親クラスのメソッドを上書きするはず」 とコンパイラに伝わり、タイポなどで誤って新メソッドを定義してしまうのを防ぎます。必ず付けるのが推奨。
7-2. interface(implements)
interface は メソッドの契約。Javaは 複数interface実装 が可能。
public interface Speakable {
void speak();
}
public interface Walkable {
void walk();
}
public class Dog implements Speakable, Walkable {
public void speak() { System.out.println("Woof!"); }
public void walk() { System.out.println("Walking"); }
}
interfaceの進化
Java 7まで: 抽象メソッドだけ持てる
Java 8: defaultメソッド・staticメソッドが書けるように
Java 9: privateメソッド(defaultメソッドの共通化)
Java 17: sealed interface
defaultメソッド(Java 8+)
public interface Greeter {
String name();
default String greet() { // デフォルト実装
return "Hello, " + name();
}
}
public class User implements Greeter {
private String name;
public String name() { return name; }
// greet() はdefaultが使われる
}
これによりinterfaceに 後方互換性を保ちつつメソッドを追加できるようになりました。Java 8で Collection に大量のdefaultメソッドが追加され、Streams APIが成立した経緯があります。
7-3. abstract classとinterfaceの使い分け
abstract class interface
───────────────── ─────────────────
extendsは1つだけimplementsは複数可
状態(フィールド) 状態は持てない(static finalのみ)
コンストラクタあり コンストラクタなし
defaultメソッドdefaultメソッド(Java 8+)
強い継承関係 契約・型として
使い分け
abstract class:
- 共通の状態 + 共通の実装を継承させたい
- 「is-a」関係が強い
interface:
- 振る舞いの契約だけ定義
- 複数の能力を組み合わせたい(多重継承相当)
- APIボーダーで使う
「まずinterface、必要ならabstract class」が現代の作法。
7-4. デフォルトメソッド(Java 8+)
public interface Container<T> {
T get(int i);
int size();
default boolean isEmpty() { return size() == 0; }
default T first() { return get(0); }
}
implementorは get と size だけ実装すれば、isEmpty first が自動的に手に入ります。
ダイヤモンド問題
複数interfaceが同名defaultメソッドを持つと曖昧。明示的に解決する必要があります。
interface A { default void hello() { System.out.println("A"); } }
interface B { default void hello() { System.out.println("B"); } }
class C implements A, B {
@Override
public void hello() {
A.super.hello(); // 明示的にAを選択
}
}
7-5. sealedクラス(Java 17+)
sealed は 「継承できる子クラスを限定する」 機能。Java 17で正式導入。
public sealed interface Shape permits Circle, Square, Triangle {}
public final class Circle implements Shape {
private final double radius;
public Circle(double r) { this.radius = r; }
}
public final class Square implements Shape {
private final double side;
public Square(double s) { this.side = s; }
}
public final class Triangle implements Shape {
private final double base, height;
public Triangle(double b, double h) { this.base = b; this.height = h; }
}
sealedの効果
permitsで許可された子クラス以外は継承できない- コンパイラが網羅性を検証できる(switch式でdefault不要に)
double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square sq -> sq.side() * sq.side();
case Triangle t -> 0.5 * t.base() * t.height();
// default不要!sealedだから網羅されている
};
}
これは 「代数的データ型(ADT)のJava流の実現」。Scalaのsealed traitやKotlinのsealed classと同じ思想で、ドメイン型を網羅的に表現できます。
permitsの3つの制約
子クラスは次のいずれかを選択する必要があります。
public final class Circle implements Shape { } // final: これ以上継承不可
public sealed class Square implements Shape permits OuterSquare, InnerSquare { } // sealed: 限定継承
public non-sealed class Triangle implements Shape { } // non-sealed: 自由継承
7-6. 多態性とオーバーライド
動的ディスパッチ
Animal a = new Dog();
a.speak(); // 実際の型Dogのメソッドが呼ばれる
宣言型は Animal だが、実体は Dog なので、実行時にどちらのメソッドが呼ばれるかが決まる。これが多態性の本質。
staticメソッドはオーバーライドできない
class A { static void m() { System.out.println("A"); } }
class B extends A { static void m() { System.out.println("B"); } }
A a = new B();
a.m(); // "A"(staticは宣言型で決まる!)
これは「staticは隠蔽(hiding)はできるがオーバーライドできない」というJavaのルール。混乱を招くのでサブクラスでstaticを再定義しないほうが安全。
finalで禁止
public class Base {
public final void doNotOverride() { ... }
}
「このメソッドはサブクラスで触れるな」を表明。テンプレートメソッドパターンで重要。
7-7. このセクションのまとめ
継承(extends):
単一継承
@Override必須(推奨)
動的ディスパッチで多態性
interface(implements):
複数実装可能
Java 8+ でdefaultメソッド
状態は持てない(static finalのみ)
abstract class vs interface:
状態が必要 → abstract class
契約だけ → interface(推奨)
「まずinterface、必要ならabstract class」
sealed(Java 17+):
permitsで子クラスを限定
ADT風の網羅的型定義
switch式でdefault不要
多態性:
実際の型のメソッドが呼ばれる
staticは隠蔽(hiding)であってオーバーライドではない
次のセクションでは、Javaの重要機能――ジェネリクスを扱います。
8. ジェネリクス
ジェネリクスはJava 5(2004)で導入された 「型をパラメータ化する」仕組み。コレクションや関数の型安全性を保ちつつ、再利用可能なコードを書けます。「型消去」というJava独特の実装 が特徴で、これを理解するとジェネリクスの挙動が腑に落ちます。
8-1. なぜジェネリクスか
ジェネリクス導入前(Java 1.4以前)の問題:
// Java 1.4
List list = new ArrayList();
list.add("hello");
list.add(42); // 何でも入れられる
String s = (String) list.get(0); // キャストが必要
String t = (String) list.get(1); // 実行時にClassCastException!
これを解決するのがジェネリクス:
// Java 5+
List<String> list = new ArrayList<>();
list.add("hello");
list.add(42); // コンパイルエラー!
String s = list.get(0); // キャスト不要
コンパイル時に型をチェックし、キャストを自動化。これがジェネリクスの基本的な価値です。
8-2. 型パラメータの基本
ジェネリッククラス
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get();
T は 型パラメータ。慣習的に1文字(T, E, K, V)を使う:
T - Type(一般)
E - Element(コレクションの要素)
K - Key
V - Value
N - Number
R - Return type
ジェネリックメソッド
public static <T> T first(List<T> list) {
return list.get(0);
}
String s = first(List.of("a", "b"));
Integer i = first(List.of(1, 2));
ダイヤモンド演算子(Java 7+)
// Java 6
Map<String, List<Integer>> m = new HashMap<String, List<Integer>>();
// Java 7+
Map<String, List<Integer>> m = new HashMap<>(); // 右辺の型を推論
8-3. 型消去(type erasure)
Javaのジェネリクスは 型消去で実装されています。コンパイル後のバイトコードでは型パラメータが消える。
List<String> list = new ArrayList<>();
list.add("hello");
// コンパイル後(バイトコード上)は
List list = new ArrayList(); // 型パラメータが消えている
list.add("hello");
なぜ型消去か
Java 5でジェネリクスを導入する際、「既存のコードと完全互換」を最優先したため。型情報を実行時に持つと、それ以前のクラスファイルとの互換性が崩れる。
型消去の影響
1. 実行時に型情報が無い
List<String> list = new ArrayList<>();
list.getClass(); // ArrayList(List<String> ではない!)
// instanceofは使えない
if (obj instanceof List<String>) { ... } // コンパイルエラー
if (obj instanceof List<?>) { ... } // OK(ワイルドカード)
2. 配列とジェネリクスは混ぜられない
List<String>[] arr = new List<String>[10]; // コンパイルエラー!
3. オーバーロードできない
class A {
void m(List<String> l) {}
void m(List<Integer> l) {} // コンパイルエラー!同じシグネチャに
}
例外: パラメータ化された型情報を持ちたいとき
// Class<T> を引数に取って実行時に型情報を取る
public <T> List<T> readList(Class<T> clazz, String json) {
...
}
List<User> users = readList(User.class, jsonString);
これがJacksonなどのJSONライブラリで User.class を渡すパターンの根拠。
8-4. 境界とワイルドカード
上限境界(extends)
「TはXのサブクラスに限定」
public <T extends Number> double sum(List<T> list) {
double total = 0;
for (T x : list) total += x.doubleValue();
return total;
}
sum(List.of(1, 2, 3)); // OK(Integer extends Number)
sum(List.of(1.0, 2.0)); // OK
sum(List.of("a", "b")); // コンパイルエラー
ワイルドカード(?)
「何らかの型」を表す。
List<?> anyList; // 何でも入るList
List<? extends Number> nums; // Numberまたはそのサブクラス
List<? super Integer> ints; // Integerまたはその親クラス
? extends T(上限ワイルドカード、共変)
「読み取り専用」的に使える。
public double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) total += n.doubleValue();
return total;
}
sum(List.of(1, 2, 3)); // List<Integer> も渡せる
sum(List.of(1.0, 2.0)); // List<Double> も渡せる
? super T(下限ワイルドカード、反変)
「書き込み専用」的に使える。
public void addIntegers(List<? super Integer> list) {
list.add(1);
list.add(2);
}
addIntegers(new ArrayList<Integer>());
addIntegers(new ArrayList<Number>()); // OK
addIntegers(new ArrayList<Object>()); // OK
8-5. PECS原則
ワイルドカードの使い分けの覚え方が PECS(Producer Extends, Consumer Super)。Joshua Bloch(『Effective Java』著者)の有名な格言。
Producer Extends: 「データを取り出す」だけなら ? extends T
Consumer Super: 「データを入れる」だけなら ? super T
// データを供給する側(読み取り)
public void copy(List<? extends T> src, List<? super T> dst) {
for (T x : src) dst.add(x);
}
// 標準ライブラリのCollections.copy
public static <T> void copy(List<? super T> dest, List<? extends T> src);
これにより、copy(List<Object> dest, List<Integer> src) のような自然な使い方ができます。
8-6. このセクションのまとめ
ジェネリクス:
Java 5で導入、型をパラメータ化
コンパイル時に型を検査、キャスト不要
型パラメータ:
T / E / K / V / N / Rなどの慣習
ジェネリッククラス + ジェネリックメソッド
型消去:
実行時に型情報は消える(互換性のため)
→ instanceofでList<String> はチェック不可
→ 配列とジェネリクスは混ぜられない
→ クラス情報が必要ならClass<T> を引数に
境界:
T extends Xで上限
ワイルドカード ? / ? extends T / ? super T
PECS原則:
Producer Extends(読み取り側)
Consumer Super(書き込み側)
次のセクションでは、Javaのコレクションフレームワークを扱います。
9. コレクションフレームワーク
Javaの Collections Framework(Java 1.2、1998年導入)は、産業界で最も成熟したコレクションライブラリのひとつ。List、Set、Map、Queue の整理された階層と、用途別の実装クラスが揃っています。
9-1. List / Set / Map / Queue / Deque
Collection(root)
├── List順序あり、重複OK (ArrayList, LinkedList)
├── Set重複なし (HashSet, TreeSet, LinkedHashSet)
├── Queue FIFO用(先頭・末尾) (LinkedList, ArrayDeque, PriorityQueue)
└── Deque両端キュー (ArrayDeque, LinkedList)
Map(root、Collectionではない)
├── HashMap無順序、O(1)
├── LinkedHashMap挿入順
├── TreeMapキー順(赤黒木)
└── ConcurrentHashMap並行向け
よく使うメソッド
List<Integer> list = new ArrayList<>();
list.add(1); // 末尾追加
list.add(0, 0); // 先頭挿入
list.get(0); // 取得
list.set(0, 100); // 更新
list.remove(0); // 削除
list.contains(1); // 含む?
list.indexOf(1); // 位置
list.size(); // サイズ
list.isEmpty(); // 空?
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.get("a"); // 1
map.getOrDefault("b", 0); // 0
map.putIfAbsent("c", 3);
map.computeIfAbsent("d", k -> 4);
map.merge("a", 10, Integer::sum); // 既存値とマージ
9-2. ArrayList vs LinkedList
ArrayList LinkedList
─────────────────────────────────────
内部構造 配列 双方向リスト
インデックスO(1) O(n)
末尾追加 償却O(1) O(1)
先頭追加O(n) O(1)
中間挿入O(n) O(n)(位置探索が遅い)
メモリ 小さい 大きい(ノードオブジェクト)
キャッシュ局所性 良い 悪い
実用的な選択はArrayListが圧倒的多数。LinkedListは理論的には先頭操作が速いが、現代のCPUでは キャッシュ局所性の良いArrayList のほうが多くの場面で速い。両端操作なら ArrayDeque が最速。
// 先頭・末尾の両方で操作したいとき
Deque<Integer> deque = new ArrayDeque<>();
deque.addFirst(1);
deque.addLast(2);
deque.pollFirst();
deque.pollLast();
9-3. HashMap vs TreeMap vs LinkedHashMap
HashMap LinkedHashMap TreeMap
──────────────────────────────────────────────────────
順序 無し 挿入順 キー順
get/put O(1) 平均O(1) 平均O(log n)
内部 ハッシュ表 ハッシュ表+リスト 赤黒木
キーの制約hashCode/equals同上ComparableまたはComparator
HashMap(最も使われる)
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
for (var entry : map.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
// 順序は不定
TreeMap(キー順が欲しいとき)
TreeMap<String, Integer> sorted = new TreeMap<>();
sorted.put("c", 3);
sorted.put("a", 1);
sorted.put("b", 2);
sorted.firstKey(); // "a"
sorted.lastKey(); // "c"
sorted.headMap("b"); // aまでのサブマップ
LinkedHashMap(挿入順保持)
Map<String, Integer> ordered = new LinkedHashMap<>();
ordered.put("c", 3);
ordered.put("a", 1);
ordered.put("b", 2);
// イテレーションは挿入順: c, a, b
LRUキャッシュとして
Map<String, Integer> lru = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Integer> eldest) {
return size() > 100;
}
};
accessOrder=true でアクセス順保持。古いエントリを自動削除するLRUキャッシュとして使えます。
9-4. 不変コレクション(List.ofなど)
Java 9で イミュータブルなコレクションが標準化されました。
List<Integer> list = List.of(1, 2, 3); // 変更不可
Set<String> set = Set.of("a", "b", "c");
Map<String, Integer> map = Map.of("a", 1, "b", 2);
list.add(4); // UnsupportedOperationException!
Java 10+ ではStreamの toList() も不変リストを返します。
List<Integer> result = numbers.stream()
.filter(n -> n > 0)
.toList(); // 不変リスト
Collections.unmodifiableXxx
List<Integer> mutable = new ArrayList<>(List.of(1, 2, 3));
List<Integer> readOnly = Collections.unmodifiableList(mutable);
readOnly.add(4); // UnsupportedOperationException
mutable.add(4); // OK!元のリストは変更可能
readOnly; // 1, 2, 3, 4が見える(ビュー)
これは「読み取り専用のビュー」で、元のリストが変更されると見える内容も変わる。注意。
9-5. IterableとIterator
すべてのコレクションは Iterable を実装し、for-each で走査できる。
public interface Iterable<T> {
Iterator<T> iterator();
}
public interface Iterator<T> {
boolean hasNext();
T next();
default void remove();
}
イテレーション中の変更
List<Integer> list = new ArrayList<>(List.of(1, 2, 3));
for (Integer x : list) {
if (x == 2) list.remove(x); // ConcurrentModificationException!
}
イテレーション中にコレクションを直接変更すると例外。Iterator経由か、Streamのfilter を使う。
// Iterator経由
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
if (it.next() == 2) it.remove(); // OK
}
// Streamで
list = list.stream().filter(x -> x != 2).toList();
9-6. このセクションのまとめ
階層:
Collection: List / Set / Queue / Deque
Map: HashMap / TreeMap / LinkedHashMap / ConcurrentHashMap
実用選択:
順序+重複OK: ArrayList(LinkedListはほぼ不要)
両端操作: ArrayDeque
重複排除: HashSet
挿入順: LinkedHashMap / LinkedHashSet
キー順ソート: TreeMap
並行アクセス: ConcurrentHashMap
不変:
List.of / Set.of / Map.of(Java 9+)
stream().toList()(Java 16+)
Collections.unmodifiable* はビュー(元が変わると変わる)
イテレーション:
for-eachで走査、変更したいならIteratorかStream
次のセクションでは、Java 8で導入された関数型プログラミングの基礎――関数型インターフェースとラムダを扱います。
10. 関数型インターフェースとラムダ
Java 8(2014)の最大の革命が ラムダ式 + Streams API。それまで「冗長で時代遅れ」と批判されることがあったJavaを、関数型プログラミングの要素を取り込んでモダン化した転換点です。
10-1. 関数型インターフェース
**関数型インターフェース(functional interface)**とは、抽象メソッドが1つだけ のinterface。これがラムダ式の「型」になります。
@FunctionalInterface
public interface Greeter {
String greet(String name);
// 抽象メソッドは1つだけ
}
// ラムダ式で実装
Greeter g = name -> "Hello, " + name;
g.greet("Alice"); // "Hello, Alice"
@FunctionalInterface アノテーションは 「抽象メソッドが1つだけ」をコンパイラに保証させる。なくてもラムダは使えるが、付けるのが推奨。
標準ライブラリの関数型interface
java.util.function パッケージに用途別の標準interfaceがあります。
| Interface | シグネチャ | 用途 |
|---|---|---|
Function<T, R> |
R apply(T) |
変換 |
Predicate<T> |
boolean test(T) |
真偽判定 |
Consumer<T> |
void accept(T) |
副作用 |
Supplier<T> |
T get() |
値の供給 |
BiFunction<T,U,R> |
R apply(T,U) |
2引数の変換 |
UnaryOperator<T> |
T apply(T) |
同型変換 |
BinaryOperator<T> |
T apply(T,T) |
同型2引数 |
10-2. ラムダ式の基本
// 引数なし
Runnable r = () -> System.out.println("hi");
// 引数1つ(括弧省略可)
Function<Integer, Integer> sq = x -> x * x;
// 引数複数
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
// 複数文({} とreturnが必要)
Function<Integer, String> classify = x -> {
if (x > 0) return "positive";
else if (x < 0) return "negative";
else return "zero";
};
// 型を明示する場合
BiFunction<Integer, Integer, Integer> mul = (Integer a, Integer b) -> a * b;
クロージャ(外側変数のキャプチャ)
String prefix = "Hello, ";
Function<String, String> greet = name -> prefix + name;
greet.apply("Alice"); // "Hello, Alice"
ラムダは外側の変数をキャプチャします。ただし 「実質的final」(実質的に変更されない変数)でなければエラー。
String prefix = "Hello, ";
prefix = "Hi, "; // 後で変更すると...
Function<String, String> greet = name -> prefix + name;
// ^^ コンパイルエラー
これは クロージャ内のスレッド安全性を保つため の設計判断。
10-3. メソッド参照
「特定のメソッドをそのままラムダとして使う」記法。コードがさらに簡潔に。
// インスタンスメソッド
Function<String, Integer> len = String::length;
// 等価: s -> s.length()
// staticメソッド
Function<String, Integer> parse = Integer::parseInt;
// コンストラクタ
Function<String, ArrayList<String>> create = ArrayList::new;
// 特定インスタンスのメソッド
String greeting = "Hello";
Function<String, String> concat = greeting::concat;
// クラス名::インスタンスメソッド(最初の引数がレシーバ)
list.stream().map(String::toUpperCase);
// 等価: list.stream().map(s -> s.toUpperCase())
10-4. java.util.functionパッケージ
主要な関数型interfaceを実例とともに整理。
import java.util.function.*;
// Function<T, R>: TをRに変換
Function<String, Integer> length = String::length;
length.apply("hello"); // 5
// Predicate<T>: Tが条件を満たすか
Predicate<String> isEmpty = String::isEmpty;
isEmpty.test(""); // true
// Consumer<T>: Tを消費する(副作用)
Consumer<String> print = System.out::println;
print.accept("hi");
// Supplier<T>: 引数なしでTを返す
Supplier<List<String>> factory = ArrayList::new;
List<String> list = factory.get();
// BiFunction<T, U, R>: 2引数を取ってRを返す
BiFunction<Integer, Integer, Integer> add = Integer::sum;
add.apply(1, 2); // 3
合成
Function<Integer, Integer> doubled = x -> x * 2;
Function<Integer, Integer> plus10 = x -> x + 10;
Function<Integer, Integer> combined = doubled.andThen(plus10);
combined.apply(5); // (5 * 2) + 10 = 20
Function<Integer, Integer> combined2 = doubled.compose(plus10);
combined2.apply(5); // (5 + 10) * 2 = 30
Predicate<Integer> isPositive = x -> x > 0;
Predicate<Integer> isEven = x -> x % 2 == 0;
isPositive.and(isEven).test(4); // true
isPositive.or(isEven).negate().test(-1); // true → false
10-5. このセクションのまとめ
関数型インターフェース:
抽象メソッドが1つだけのinterface
@FunctionalInterfaceでコンパイラ保証
ラムダ式:
() -> ...
x -> x * 2
(a, b) -> a + b
外側の変数は実質的finalならキャプチャ可
メソッド参照:
String::length(インスタンスメソッド)
Integer::parseInt(staticメソッド)
ArrayList::new(コンストラクタ)
主要な関数型interface:
Function<T, R> 変換
Predicate<T> 真偽判定
Consumer<T> 副作用
Supplier<T> 値の供給
BiFunction<T,U,R> 2引数変換
合成:
andThen / compose / and / or / negate
次のセクションでは、Java 8のもう一つの中核――Streams APIを扱います。
11. Streams API
Streams API(Java 8、2014)は 「コレクションを宣言的に処理する」ための仕組み。Linuxのパイプや関数型の map/filter/reduce をJavaで実現したものです。書きやすさ・読みやすさ・並列化が一気に得られます。
11-1. Streamの概念
Stream は「要素の流れ」を表す抽象。コレクションとは違い、遅延評価で 1度しか使えないのが特徴。
List<String> names = List.of("Alice", "Bob", "Charlie");
List<String> upper = names.stream() // Stream<String>
.filter(n -> n.length() > 3) // 中間操作
.map(String::toUpperCase) // 中間操作
.collect(Collectors.toList()); // 終端操作
// ["ALICE", "CHARLIE"]
コレクションとStreamの違い
Collection Stream
──────────────────────────────
データの集合 データの流れ
何度でも走査可1度のみ
イーガー(即時) 遅延(lazy)
データの保持 処理パイプライン
11-2. 中間操作と終端操作
中間操作(intermediate): 別のStreamを返す
filter(predicate) 条件で絞り込み
map(function) 要素を変換
flatMap(function) 入れ子を平坦化
sorted() ソート
distinct() 重複除去
limit(n) 先頭n個
skip(n) 先頭n個をスキップ
peek(consumer) 副作用(デバッグ用)
終端操作(terminal): Streamを消費して結果を返す
collect(collector) 結果を集める(List/Map/Setなど)
forEach(consumer) 各要素に副作用
reduce(...) 畳み込み
count() 個数
sum() / min() / max() / average()
findFirst() / findAny()
allMatch / anyMatch / noneMatch
toArray()
toList()(Java 16+)
遅延評価
中間操作は 終端操作が呼ばれるまで実行されない。
Stream<String> s = list.stream()
.filter(x -> { System.out.println("filter " + x); return true; })
.map(x -> { System.out.println("map " + x); return x; });
// ここまでは何も出力されない
s.toList(); // ここで初めて実行される
11-3. map / filter / collect / reduce
map: 変換
List<String> upper = names.stream()
.map(String::toUpperCase)
.toList();
filter: 絞り込み
List<Integer> positives = numbers.stream()
.filter(n -> n > 0)
.toList();
collect: 集約
// Listに集める
List<String> list = stream.toList();
// Setに集める
Set<String> set = stream.collect(Collectors.toSet());
// Mapに集める
Map<String, Integer> byLength = names.stream()
.collect(Collectors.toMap(n -> n, String::length));
// グループ化
Map<Integer, List<String>> byLen = names.stream()
.collect(Collectors.groupingBy(String::length));
// カウント
Map<Integer, Long> counts = names.stream()
.collect(Collectors.groupingBy(String::length, Collectors.counting()));
// 結合
String joined = names.stream()
.collect(Collectors.joining(", ", "[", "]"));
// "[Alice, Bob, Charlie]"
reduce: 畳み込み
int sum = numbers.stream().reduce(0, Integer::sum);
int max = numbers.stream().reduce(Integer.MIN_VALUE, Integer::max);
// Optional版
Optional<Integer> maxOpt = numbers.stream().reduce(Integer::max);
flatMap: 入れ子の平坦化
List<List<Integer>> nested = List.of(
List.of(1, 2),
List.of(3, 4),
List.of(5)
);
List<Integer> flat = nested.stream()
.flatMap(List::stream)
.toList(); // [1, 2, 3, 4, 5]
11-4. parallelStream
parallelStream() で 並列処理化できる。Fork/Join Frameworkが裏で動きます。
long sum = numbers.parallelStream()
.mapToLong(Integer::longValue)
.sum();
parallelStreamの落とし穴
- 小さなコレクションでは遅い(並列化のオーバーヘッド)
- 副作用のある操作は危険(順序保証なし、データ競合)
- 共有ForkJoinPoolを使うので他の並列処理と取り合いになる
「CPUバウンド + 大量要素 + 副作用なし」の場合だけ効果がある、というのが現実的なライン。安易に並列化するとむしろ遅くなる。
11-5. 落とし穴
Streamは1度しか使えない
Stream<String> s = list.stream();
s.count();
s.count(); // IllegalStateException: stream has already been operated upon
何度も使いたいなら、毎回 list.stream() で作り直す。
null要素
List<String> list = Arrays.asList("a", null, "b");
list.stream().count(); // 3(OK)
list.stream().sorted().toList(); // NullPointerException
null要素は予期しないところでNPEを起こす。Optional でラップするか、事前に filter(Objects::nonNull) で除外。
parallelStreamの罠
// Bad: forEachは順序保証なし
list.parallelStream().forEach(System.out::println); // 順序がバラバラ
// Good: forEachOrderedは順序保証あり(並列化の意味は薄れる)
list.parallelStream().forEachOrdered(System.out::println);
11-6. このセクションのまとめ
Stream:
要素の流れ、遅延評価、1度のみ使用可
pipeline: source → 中間操作 → 終端操作
中間操作:
filter / map / flatMap / sorted / distinct / limit / skip
終端操作:
collect / reduce / forEach / count / findFirst / toList
Collectors:
toList / toSet / toMap / groupingBy / counting / joining
parallelStream:
CPUバウンド + 大量要素 + 副作用なしのときのみ有効
小さなコレクションでは逆に遅い
落とし穴:
Streamは1度のみ
null要素でNPE
forEachは順序保証なし → forEachOrdered
次のセクションでは、Javaのnull安全性の主役Optionalを扱います。
12. Optionalとnull安全
null の扱いを根本から変えるJava 8の機能。「値があるか分からない」を型で表現するアプローチです。
12-1. Optionalの動機
第3章3-4で触れたnull問題。Optional はこれに対するJavaの答え。
// 旧来: メソッドがnullを返すかどうか型から分からない
public User findById(int id) { ... return null; }
User u = findById(1);
u.getName(); // NPEの可能性!
// Optional: 型で「ない可能性」を表現
public Optional<User> findById(int id) {
return Optional.ofNullable(...);
}
Optional<User> u = findById(1);
String name = u.map(User::getName).orElse("anonymous");
Optional<T> を返り値の型にすることで、呼び出し側がnullチェックを忘れないようになります。
12-2. 主要API
// 生成
Optional.of(value) // valueがnullだとNPE
Optional.ofNullable(value) // valueがnullならempty
Optional.empty() // 空
// 取り出し(注意して使う)
opt.get() // 値、空ならNoSuchElementException
opt.orElse(default) // 空ならデフォルト
opt.orElseGet(supplier) // 空ならサプライヤを呼ぶ(lazy)
opt.orElseThrow() // 空ならNoSuchElementException
opt.orElseThrow(MyException::new)
// 検査
opt.isPresent() // 値があるか
opt.isEmpty() // 空か(Java 11+)
opt.ifPresent(consumer) // 値があれば消費
opt.ifPresentOrElse(c1, runnable) // Java 9+
// 変換
opt.map(function) // T → U
opt.flatMap(function) // Optional<T> → Optional<U>
opt.filter(predicate) // 条件で空に
典型的な使い方
Optional<User> user = userRepo.findById(1);
// パターン1: ifPresent
user.ifPresent(u -> notify(u));
// パターン2: map + orElse
String name = user.map(User::getName).orElse("anonymous");
// パターン3: 連鎖
String city = user
.map(User::getAddress)
.map(Address::getCity)
.orElse("unknown");
// パターン4: 例外を投げる
User u = user.orElseThrow(() -> new UserNotFoundException(1));
12-3. アンチパターン
1. Optionalを引数にする
// Bad
public void process(Optional<User> user) { ... }
// Good
public void process(User user) { ... }
public void process() { ... } // オーバーロードか
Optional は 戻り値専用。引数にすると呼び出し側で Optional.of(x) のラッピングが必要になり、無駄な抽象化。
2. フィールドにする
// Bad
class User {
private Optional<String> middleName;
}
// Good(null許容)
class User {
private String middleName; // nullになりうる
}
Optional はシリアライズできず、フィールドに使うと不自然。
3. isPresent + get
// Bad(nullチェックと変わらない)
if (opt.isPresent()) {
User u = opt.get();
process(u);
}
// Good
opt.ifPresent(this::process);
isPresent + get は 「Optionalの意味を消している」ので避ける。map ifPresent orElse を使う。
4. orElse(expensive())
// Bad: orElseは引数を常に評価する
opt.orElse(expensiveDefault());
// Good: orElseGetは空のときだけ評価する
opt.orElseGet(() -> expensiveDefault());
12-4. このセクションのまとめ
動機:
nullの代わりに「値があるかも」を型で表現
呼び出し側のチェックを強制
API:
生成: of / ofNullable / empty
取り出し: get / orElse / orElseGet / orElseThrow
検査: isPresent / ifPresent
変換: map / flatMap / filter
アンチパターン:
引数に使わない(戻り値専用)
フィールドに使わない
isPresent + get(map / ifPresentを使う)
orElse(expensive()) → orElseGet(() -> expensive())
次のセクションでは、モダンJavaの表現力を支えるrecordとsealedを扱います。
13. recordとsealed(モダンJavaの表現力)
Java 14(record)とJava 17(sealed)で導入された機能は、「データクラス」と「代数的データ型」をJavaらしく実現したもの。Java 21のパターンマッチングと組み合わせると、Scala/Kotlinに並ぶ表現力が手に入ります。
13-1. record(Java 14+)
動機
それまでJavaで値クラスを書くのは冗長でした。
// 古典的な値クラス(10行以上のboilerplate)
public final class Point {
private final int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int getX() { return x; }
public int getY() { return y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
@Override public String toString() { return "Point(" + x + ", " + y + ")"; }
}
これがrecordで1行に。
public record Point(int x, int y) {}
recordの自動生成
public record Point(int x, int y) {}
// 自動的に生成されるもの:
// - private final int x, y;
// - public Point(int x, int y) { ... } コンストラクタ
// - public int x() { return x; } アクセサ(getterではなくx())
// - public int y() { return y; }
// - equals / hashCode / toString
コンパクトコンストラクタ(バリデーション)
public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) throw new IllegalArgumentException();
}
}
public Point { ... } という特殊な構文で、コンストラクタの引数代入の前にバリデーションを書ける。
recordの制約
- イミュータブル(フィールドはfinal)
- 継承できない(
record自身が暗黙的にfinal) - 状態を追加できない(インスタンスフィールドの追加禁止)
- interfaceはimplements可能
public record Point(int x, int y) implements Comparable<Point> {
@Override
public int compareTo(Point o) {
return Integer.compare(x * x + y * y, o.x * o.x + o.y * o.y);
}
public double distanceFromOrigin() {
return Math.sqrt(x * x + y * y);
}
}
13-2. sealedクラス・interface(Java 17+)
第7章7-5でも触れたsealed。「継承可能な子クラスを限定する」機能。
public sealed interface Shape permits Circle, Square, Triangle {}
public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
sealed + recordの組み合わせ
これは 代数的データ型(ADT)。Haskellの data Shape = Circle Double | Square Double | Triangle Double Double に相当。
public sealed interface Result<T> {
record Ok<T>(T value) implements Result<T> {}
record Err<T>(String message) implements Result<T> {}
}
これが モダンJavaの関数型表現の核。
13-3. パターンマッチングfor switch(Java 21)
sealed + record + switch の組み合わせで、網羅的なパターンマッチングができます。
sealed interface Shape permits Circle, Square, Triangle {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}
record Triangle(double base, double height) implements Shape {}
double area(Shape s) {
return switch (s) {
case Circle(double r) -> Math.PI * r * r;
case Square(double side) -> side * side;
case Triangle(double b, double h) -> 0.5 * b * h;
// sealedなのでdefault不要!コンパイラが網羅性を検証
};
}
これが Scala/Kotlinに近づいた現代Java。Result のような型を作って関数型のエラーハンドリングがJavaらしく書けます。
sealed interface Result<T> {}
record Ok<T>(T value) implements Result<T> {}
record Err<T>(String message) implements Result<T> {}
String describe(Result<Integer> r) {
return switch (r) {
case Ok<Integer>(Integer v) -> "value: " + v;
case Err<Integer>(String m) -> "error: " + m;
};
}
13-4. このセクションのまとめ
record(Java 14+):
値クラスを1行で書ける
equals / hashCode / toString / アクセサが自動生成
コンパクトコンストラクタでバリデーション
イミュータブル、継承不可
sealed(Java 17+):
permitsで子クラスを限定
switch式で網羅性チェック
組み合わせ(Java 21):
sealed interface + recordでADT
switch式 + 分解パターンで関数型風の処理
→ Scala / Kotlinに近づいた現代Java
次のセクションでは、Javaの例外処理――checked / unchecked、try-with-resourcesを扱います。
14. 例外処理
Javaの例外処理は 「checkedとuncheckedの二分」 という独特の設計を持ちます。これはJavaの特徴であり、賛否両論ある設計判断です。
14-1. checked vs unchecked
Throwable
├── Error JVMレベルの致命的エラー(OOM、StackOverflow)
└── Exception
├── checked 「callerがハンドリングする義務」のある例外
└── RuntimeException unchecked、ハンドリング義務なし
checked例外
メソッドが throws で宣言する必要があり、呼び出し側でcatchするかthrowsで再宣言する義務がある。
public void readFile(String path) throws IOException {
Files.readAllLines(Path.of(path)); // IOExceptionを投げる
}
// 呼び出し側
try {
readFile("data.txt");
} catch (IOException e) {
e.printStackTrace();
}
代表的なchecked例外: IOException、SQLException、ClassNotFoundException。
unchecked例外(RuntimeException)
throws 不要。catchも任意。
public int divide(int a, int b) {
return a / b; // ArithmeticExceptionが起きうるが宣言不要
}
代表的なunchecked: NullPointerException、IllegalArgumentException、IndexOutOfBoundsException、ClassCastException。
checked例外の論争
Javaの 「checked例外を強制する」 という設計は、長らく議論の的でした。
利点:
「失敗しうるAPI」が型として明確
呼び出し側にエラー処理を強制
欠点:
ボイラープレートが増える(throwsの伝播)
ラムダ・Streamと相性が悪い(throwsできない)
実際はcatchしてRuntimeExceptionに包み直すだけになる
近年の流れは 「uncheckedにする」 方が支配的。Spring・Hibernateなどのフレームワークは独自のRuntimeExceptionを投げる方針を採っており、Kotlinはchecked例外を完全に廃止しました。
14-2. try / catch / finally
try {
risky();
} catch (IOException e) {
log.error("IO error", e);
} catch (SQLException e) {
log.error("SQL error", e);
} finally {
cleanup(); // 例外の有無にかかわらず必ず実行
}
finally ブロックは 必ず実行される(System.exit やJVM強制終了を除く)。リソース解放に伝統的に使われていました(Java 7以降はtry-with-resourcesが推奨)。
14-3. try-with-resources
Java 7で導入された リソース自動解放の仕組み。AutoCloseable を実装したオブジェクトは、tryブロックを抜けるときに自動で close() が呼ばれる。
// 旧来の書き方
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("a.txt"));
return reader.readLine();
} finally {
if (reader != null) reader.close(); // closeもIOExceptionを投げうる
}
// try-with-resources
try (BufferedReader reader = new BufferedReader(new FileReader("a.txt"))) {
return reader.readLine();
} // 自動的にcloseされる(例外があっても)
複数リソースも書ける。
try (var in = Files.newBufferedReader(Path.of("a.txt"));
var out = Files.newBufferedWriter(Path.of("b.txt"))) {
out.write(in.readLine());
} // out, inの順にcloseされる
これが Pythonの with、Rubyの with ブロックに相当するJavaの解決策。
14-4. multi-catchと例外チェイン
multi-catch(Java 7+)
try {
...
} catch (IOException | SQLException e) { // 複数を1つでcatch
log.error("error", e);
}
例外チェイン
try {
parse();
} catch (NumberFormatException e) {
throw new ParseException("invalid input", e); // 元の例外をchain
}
// 受け取る側
try {
process();
} catch (ParseException e) {
Throwable cause = e.getCause(); // 元のNumberFormatException
}
Throwable.getCause() で元の例外をたどれる。スタックトレースも Caused by: で表示される。
addSuppressed(try-with-resources関連)
try-with-resourcesで本体例外とclose例外の両方が起きると、close例外は addSuppressed に追加されます。
try {
...
} catch (Exception e) {
e.printStackTrace(); // Suppressed: ... と表示される
}
14-5. このセクションのまとめ
checked vs unchecked:
checked: throwsで宣言、呼び出し側でcatch義務(IOExceptionなど)
unchecked (RuntimeException): 義務なし
近年はunchecked寄り(フレームワークもRuntimeExceptionを投げる)
try / catch / finally:
finallyは必ず実行
リソース解放はtry-with-resources(Java 7+)
try-with-resources:
AutoCloseableを実装したオブジェクトのcloseを自動
複数リソースは ; で区切る
multi-catch:
catch (A | B e)
例外チェイン:
throw new X("...", originalException)
getCause() で元の例外
addSuppressedで抑制された例外
次のセクションでは、Javaの並行・並列処理(Virtual Threads含む)を扱います。
15. 並行・並列処理
Javaは当初から マルチスレッドを言語に組み込んだ先駆的な言語。長年OSスレッドベースで進化してきたが、Java 21で Virtual Threads(Project Loom) が登場し、並行処理の景色が一変しました。
15-1. Threadの基本
Thread t = new Thread(() -> {
System.out.println("running");
});
t.start();
t.join(); // 終了を待つ
RunnableとThread
// Runnable
Runnable task = () -> System.out.println("hi");
new Thread(task).start();
// 古典的: Threadを継承(推奨されない)
class MyThread extends Thread {
@Override public void run() { ... }
}
「Threadを継承するよりRunnableを実装」が現代の作法。
Threadのライフサイクル
NEW → RUNNABLE → BLOCKED / WAITING / TIMED_WAITING → TERMINATED
Threadの落とし穴
- Thread.stopは廃止(中途半端な状態でデータを壊す)
- interruptで協調的に止める: スレッドが
Thread.interrupted()をチェックして自分で終わる
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 仕事をする
}
});
t.start();
t.interrupt(); // 終了を要求(協調的)
15-2. synchronizedとvolatile
synchronized
「同時に1つのスレッドだけクリティカルセクションに入れる」モニタロック。
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int get() {
synchronized (this) {
return count;
}
}
}
synchronized はメソッドレベル・ブロックレベルどちらでも使える。メソッドの場合はthisをロック、staticメソッドは クラスオブジェクトをロックする。
volatile
メモリ可視性を保証する修飾子。スレッド間でフィールドの最新値を確実に見えるようにする。
public class Switch {
private volatile boolean on = false;
public void turnOn() { on = true; }
public boolean isOn() { return on; } // 別スレッドからの変更が見える
}
volatile は 「アトミック操作」を保証しない。インクリメントなどの複合操作には使えず、synchronized か AtomicInteger を使う。
java.util.concurrent.atomic
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // アトミックな ++count
count.get();
count.compareAndSet(0, 100); // CAS(Compare-And-Set)
CAS(Compare-And-Set)はロックフリーな並行制御の基盤。
15-3. ExecutorService / ThreadPoolExecutor
「Threadを直接newする」のではなく、スレッドプール経由で管理するのが現代のセオリー。
ExecutorService executor = Executors.newFixedThreadPool(10);
Future<Integer> future = executor.submit(() -> {
Thread.sleep(1000);
return 42;
});
Integer result = future.get(); // ブロックして待つ
executor.shutdown();
Executorsのファクトリメソッド
| メソッド | 用途 |
|---|---|
newFixedThreadPool(n) |
固定サイズ |
newCachedThreadPool() |
必要に応じて拡張 |
newSingleThreadExecutor() |
単一スレッド |
newScheduledThreadPool(n) |
定期実行 |
newVirtualThreadPerTaskExecutor() |
Virtual Thread(Java 21+) |
try-with-resourcesで自動クローズ(Java 19+)
try (var executor = Executors.newFixedThreadPool(10)) {
executor.submit(() -> doWork());
} // 自動的にshutdownされる
15-4. Fork/Join Framework
分割統治アルゴリズムを並列化するためのフレームワーク(Java 7+)。parallelStream の裏でも使われています。
ForkJoinPool pool = ForkJoinPool.commonPool();
class SumTask extends RecursiveTask<Long> {
long[] arr;
int start, end;
@Override
protected Long compute() {
if (end - start <= 100) {
long sum = 0;
for (int i = start; i < end; i++) sum += arr[i];
return sum;
}
int mid = (start + end) / 2;
SumTask left = new SumTask(arr, start, mid);
SumTask right = new SumTask(arr, mid, end);
left.fork();
return right.compute() + left.join();
}
}
long sum = pool.invoke(new SumTask(arr, 0, arr.length));
「ワークスティーリング」アルゴリズムで、空いたスレッドが他のスレッドのタスクを盗んで実行することで効率化。
15-5. CompletableFuture
Java 8で導入された 非同期プログラミングの中核。Promise/Futureの合成パターンが書けます。
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> fetchUser(1)) // 非同期実行
.thenApply(user -> user.getName()) // 連鎖変換
.thenApply(String::toUpperCase)
.exceptionally(e -> "anonymous"); // エラーハンドリング
String name = future.join(); // 結果取得
主要メソッド
// 連鎖
.thenApply(fn) // T → Uに変換
.thenAccept(consumer) // 副作用
.thenRun(runnable) // 何もせず次へ
.thenCompose(fn) // T → CompletableFuture<U>(flatMap相当)
// 並列
.thenCombine(other, fn) // 2つを待って結合
CompletableFuture.allOf(f1, f2, f3); // すべて完了を待つ
CompletableFuture.anyOf(f1, f2, f3); // 最初の1つを待つ
// エラーハンドリング
.exceptionally(fn)
.handle((value, error) -> ...)
.whenComplete((value, error) -> ...)
CompletableFutureは JavaScriptのPromiseによく似たAPI。連鎖と合成で複雑な非同期処理が書けます。
15-6. Virtual Threads(Project Loom、Java 21)
Java 21の最大の機能。OSスレッドではなく、JVMが管理する超軽量スレッド。
// 従来: OSスレッド(重い、数千が限界)
Thread.ofPlatform().start(() -> ...);
// Virtual Thread(軽量、数百万も可能)
Thread.ofVirtual().start(() -> ...);
// ExecutorServiceとして
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
HttpClient.newHttpClient().send(request, ...);
});
}
} // 1万本のVirtual Threadを並行実行!
仕組み
Virtual Threadは 「JVM上で動くユーザレベルスレッド」。OSスレッド(プラットフォームスレッドと呼ぶ)に マウント・アンマウントされる形で動きます。I/O待ちで自動的にアンマウントされ、別の仮想スレッドが同じOSスレッドを使う。
Virtual Thread 1 ─┐
Virtual Thread 2 ─┼─→ OSスレッド(少数)
Virtual Thread 3 ─┘
I/O待ちで自動アンマウント
別Virtual Threadが同じOSスレッドを使う
何が変わるか
従来:
「I/Oが多いサーバは非同期化(CompletableFuture / リアクティブ)」が必要
→ コードが複雑、デバッグ困難
Virtual Threads:
「同期的に書ける」のに何百万本でも並行できる
→ シンプルなコードで高並行
JavaScriptのasync/awaitやGoのgoroutine相当のことが、通常のThread APIそのままで書けるのが革命的。Spring Boot 3.2+ でVirtual Threadsサポートが入り、現在普及中。
注意点
- CPUバウンドには効果なし(OSスレッドと同じ)
- synchronizedでピン留めされることがある(OSスレッドが解放されない)→ Java 21後期で改善
- 既存のThread Local利用パターンには注意(数百万あると無駄)
15-7. Structured Concurrency
Java 21で プレビュー機能として導入された Structured Concurrency(構造化並行性)。並行タスクのライフサイクルをスコープで管理する仕組み。
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<User> userTask = scope.fork(() -> fetchUser(id));
Subtask<List<Order>> ordersTask = scope.fork(() -> fetchOrders(id));
scope.join(); // すべて完了を待つ
scope.throwIfFailed(); // 失敗があれば例外
User user = userTask.get();
List<Order> orders = ordersTask.get();
return new UserOrders(user, orders);
}
scope がスコープアウトすると、未完了のタスクは 自動キャンセルされる。Pythonの TaskGroup やRubyのAsync相当の仕組み。
15-8. このセクションのまとめ
Thread:
Threadの継承よりRunnableを実装
interruptで協調的に終了
同期:
synchronizedでモニタロック(thisまたはclass object)
volatileで可視性(アトミック性は保証しない)
AtomicXxxでCASベースのロックフリー
ExecutorService:
Executors.newFixedThreadPool(n) などで作る
try-with-resourcesで自動shutdown(Java 19+)
CompletableFuture:
非同期プログラミングの中核
thenApply / thenCompose / allOf / anyOf
Virtual Threads(Java 21):
JVM管理の超軽量スレッド、数百万本可能
同期的なコードのまま高並行
I/Oバウンドで効果絶大、CPUバウンドには効果なし
「Javaのasync/await」ともいえる革命
Structured Concurrency(Java 21プレビュー):
スコープで並行タスクのライフサイクル管理
失敗時の自動キャンセル
次のセクションでは、Java 9で導入された大型機能――モジュールシステムを扱います。
16. モジュールシステム(Project Jigsaw)
Java 9(2017)で導入された モジュールシステムは、Java史上最大の構造変更。JDK自体がモジュール化された大事業でした。
16-1. モジュールの動機
Java 8までの問題:
- 巨大なクラスパス: rt.jarが肥大化(数十MB)
- 依存の不明瞭さ: ライブラリの依存関係が実行時まで分からない
- カプセル化の不徹底:
internalパッケージにユーザがアクセスできてしまう - JDKの起動時間とメモリ消費
これらを解決するのが Project Jigsaw(モジュールシステム)。
16-2. module-info.java
モジュールは module-info.java を持つ独立した単位。
// module-info.java
module com.example.app {
requires java.sql; // 依存するモジュール
requires com.fasterxml.jackson.databind;
exports com.example.app.api; // 公開するパッケージ
exports com.example.app.internal to com.example.tools; // 限定公開
opens com.example.app.model; // リフレクション許可
uses com.example.app.spi.Plugin; // SPI利用
provides com.example.app.spi.Plugin
with com.example.app.impl.MyPlugin; // SPI提供
}
主要なディレクティブ
| キーワード | 意味 |
|---|---|
requires |
別モジュールに依存 |
requires transitive |
推移的依存(こちらに依存するモジュールにも見える) |
exports |
パッケージを公開 |
exports ... to ... |
限定的に公開 |
opens |
実行時リフレクションを許可 |
uses |
SPI(Service Provider Interface)を使う |
provides ... with ... |
SPI実装を提供 |
16-3. 名前付きモジュールと自動モジュール
Java 9以降、世界には3種類のモジュールがあります。
1. 名前付きモジュール(named module)
module-info.javaを持つ正式なモジュール
2. 自動モジュール(automatic module)
module-info.javaは無いが、モジュールパスに置かれたjar
→ 全パッケージを自動exportする暫定的扱い
3. 無名モジュール(unnamed module)
クラスパス上のすべてのクラス(伝統的な世界)
これは 段階的な移行を可能にする設計。古いjarをそのまま使えますが、自動モジュールを多用すると依存関係が不明瞭なので、本来は名前付きにすべき。
16-4. 強い等価性とリフレクション
モジュール化により、「リフレクションでも内部にアクセスできない」ようになりました。これが既存ライブラリ(Spring, Hibernate, Lombokなど)に大きな影響を与え、移行の難所になりました。
// Java 8まで動いた
Field f = String.class.getDeclaredField("value");
f.setAccessible(true); // 内部にアクセス
// Java 9+ でモジュールが拒否すればInaccessibleObjectException
回避: --add-opens
java --add-opens java.base/java.lang=ALL-UNNAMED ...
実行時オプションで モジュールの壁を一時的に開けることができる。Springなどのフレームワークが内部で必要としているため、しばしば見かける指定。
16-5. このセクションのまとめ
動機:
巨大なrt.jar、不明瞭な依存、カプセル化の不徹底
→ モジュール化で整理
module-info.java:
requires / exports / opens / uses / provides
3種類のモジュール:
名前付き / 自動(jarをモジュールパスに) / 無名(クラスパス)
段階的移行を可能に
リフレクション:
Java 9+ で内部アクセスは制限される
--add-opensで必要な箇所だけ開ける
実用性:
実は普通のアプリではmodule-infoを書かないことが多い
ライブラリ側は対応必須
普及度は限定的だが、JDK自体のモジュール化メリットは大きい
次のセクションでは、Javaのビルドツール――MavenとGradleを扱います。
17. ビルドと依存管理(Maven / Gradle)
Javaの世界では Maven と Gradle が二大ビルドツール。両者の特徴と選び方を解説します。
17-1. Maven
Maven(2004年〜)は 「Convention over Configuration」 をJavaに持ち込んだビルドツール。pom.xml というXMLファイルで設定。
プロジェクト構造(Maven標準)
my-app/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/ 本体コード
│ │ └── resources/ リソース
│ └── test/
│ ├── java/ テストコード
│ └── resources/
└── target/ ビルド成果物
主要コマンド
mvn compile # コンパイル
mvn test # テスト実行
mvn package # jar作成
mvn install # ローカルリポジトリにインストール
mvn clean # targetを削除
mvn dependency:tree # 依存ツリー表示
強み
- 一貫した規約: 多くのプロジェクトが同じ構造
- 成熟したエコシステム: 多数のプラグイン
- 依存解決が確実
弱み
- XMLが冗長
- 動的な設定が書きにくい
- ビルドが遅め
17-2. Gradle
Gradle(2008年〜)はMavenの弱点を解消した次世代ビルドツール。GroovyまたはKotlin DSL で設定を書けます。
gradle build # ビルド
gradle test # テスト
gradle run # 実行
gradle dependencies # 依存
gradle wrapper # ラッパー作成
強み
- DSLでプログラマブル
- インクリメンタルビルドが速い
- Android開発の標準
- 柔軟な拡張性
弱み
- 学習コストが高め
- 設定の自由度が高すぎてカオスになりがち
- エラーメッセージが分かりにくいことがある
17-3. pom.xmlとbuild.gradle.kts
pom.xml(Maven)
<?xml version="1.0" encoding="UTF-8"?>
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
build.gradle.kts(Gradle Kotlin DSL)
plugins {
id("java")
id("org.springframework.boot") version "3.2.0"
}
group = "com.example"
version = "1.0.0"
java {
sourceCompatibility = JavaVersion.VERSION_21
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}
tasks.test {
useJUnitPlatform()
}
Kotlin DSL(build.gradle.kts)が現代的な選択。Groovy DSL(build.gradle)も依然多く使われています。
17-4. 依存スコープ
Mavenの主要スコープ:
compile (デフォルト)コンパイル・実行・テストすべて
providedコンパイル時のみ(実行環境が提供)
runtime実行時のみ
testテストのみ
systemローカルパス指定
Gradleの主要スコープ:
implementation公開しない依存(推奨)
api公開する依存
compileOnlyコンパイル時のみ
runtimeOnly実行時のみ
testImplementationテスト用
implementation と api の違いはGradleの特徴で、**「依存推移を制御」**できます。api は他のモジュールに依存が見える、implementation は隠れる。
17-5. このセクションのまとめ
Maven:
XML(pom.xml)で設定
規約重視、成熟、エコシステム豊富
歴史と安定性
Gradle:
Groovy / Kotlin DSL
プログラマブル、インクリメンタルビルド
Android標準
柔軟だが学習コスト高め
選び方:
既存プロジェクト: そのまま
新規 + シンプル: Maven
新規 + 大規模 / マルチモジュール: Gradle
Android: Gradle一択
依存スコープ:
Maven: compile / provided / runtime / test
Gradle: implementation / api / compileOnly / testImplementation
次のセクションでは、Javaのテスト戦略――JUnit 5 / Mockito / AssertJを扱います。
18. テスト戦略
Java界のテストフレームワークは JUnitが圧倒的標準。現在は JUnit 5(Jupiter) が主流で、これに Mockito(モック)と AssertJ(流暢なアサーション)を組み合わせるのが定番です。
18-1. JUnit 5
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void testAdd() {
assertEquals(3, 1 + 2);
}
@Test
void testNegative() {
assertTrue(-1 < 0);
assertNotNull("hello");
}
@Test
void testException() {
assertThrows(ArithmeticException.class, () -> {
int x = 1 / 0;
});
}
}
よく使うアサーション
assertEquals(expected, actual);
assertNotEquals(...);
assertTrue(condition);
assertFalse(condition);
assertNull(value);
assertNotNull(value);
assertSame(expected, actual); // == 比較
assertArrayEquals(arr1, arr2);
assertThrows(Class, executable);
assertAll(() -> ..., () -> ...); // 複数アサーションをまとめて実行
ライフサイクル
class MyTest {
@BeforeAll
static void setupAll() { ... } // クラス全体で1度
@BeforeEach
void setup() { ... } // 各テスト前
@Test
void test1() { ... }
@AfterEach
void teardown() { ... }
@AfterAll
static void teardownAll() { ... }
}
パラメータ化テスト
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testEven(int x) {
assertTrue(x > 0);
}
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"5, 5, 10"
})
void testAdd(int a, int b, int expected) {
assertEquals(expected, a + b);
}
表示名
@Test
@DisplayName("正の整数同士を足すと正になる")
void testPositiveAdd() { ... }
18-2. Mockito
依存先(DB、API)をモック化するライブラリ。
import static org.mockito.Mockito.*;
@Test
void testNotify() {
// モック作成
Mailer mailer = mock(Mailer.class);
// 振る舞いを定義
when(mailer.send("hi")).thenReturn(true);
// テスト対象
Notifier notifier = new Notifier(mailer);
boolean result = notifier.notify("hi");
// 検証
assertTrue(result);
verify(mailer).send("hi"); // sendが "hi" で呼ばれたことを確認
verify(mailer, times(1)).send(anyString());
}
よく使うAPI
when(obj.method()).thenReturn(value);
when(obj.method()).thenThrow(new Exception());
when(obj.method(anyInt())).thenAnswer(inv -> ...);
verify(obj).method();
verify(obj, times(2)).method();
verify(obj, never()).method();
verifyNoMoreInteractions(obj);
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
verify(obj).method(captor.capture());
String captured = captor.getValue();
スパイ(部分モック)
List<String> spy = spy(new ArrayList<>());
spy.add("a"); // 実際のaddが呼ばれる
when(spy.size()).thenReturn(100);
spy.size(); // 100(モック)
18-3. AssertJ
JUnitのアサーションより流暢で読みやすいライブラリ。
import static org.assertj.core.api.Assertions.*;
@Test
void test() {
String s = "Hello, World";
assertThat(s)
.isNotNull()
.startsWith("Hello")
.contains("World")
.endsWith("d")
.hasSize(12);
List<Integer> list = List.of(1, 2, 3, 4, 5);
assertThat(list)
.hasSize(5)
.contains(3)
.doesNotContain(10)
.startsWith(1)
.allMatch(x -> x > 0);
assertThatThrownBy(() -> someMethod())
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("invalid");
}
assertThat(...). で連鎖して書ける。JUnitのアサーションより圧倒的に読みやすいので、現代のJavaではAssertJ採用が多い。
18-4. 統合テスト
Spring Boot Test
@SpringBootTest
class UserServiceTest {
@Autowired
UserService userService;
@MockBean
UserRepository repo;
@Test
void testFind() {
when(repo.findById(1)).thenReturn(Optional.of(new User("Alice")));
assertThat(userService.findById(1)).isEqualTo(new User("Alice"));
}
}
Testcontainers(DB / 外部サービスをコンテナで)
@Testcontainers
class DatabaseTest {
@Container
PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Test
void testWithRealDb() {
String url = postgres.getJdbcUrl();
// 実DBに接続してテスト
}
}
「統合テストでも本物に近い環境」をDockerコンテナで自動構築。CIでも動く。
18-5. このセクションのまとめ
JUnit 5:
@Test / @BeforeEach / @AfterEach / @BeforeAll / @AfterAll
assertEquals / assertThrows / assertAll
@ParameterizedTestで同じテストを複数入力で
Mockito:
mock(Class) でモック作成
when().thenReturn() で振る舞い定義
verify() で呼び出し検証
ArgumentCaptorで引数取得
spyで部分モック
AssertJ:
assertThat(...).method().method() の流暢な書き方
JUnitのアサーションより読みやすい
統合テスト:
@SpringBootTestでSpringコンテナ立ち上げ
Testcontainersで実DB / 外部サービス
次のセクションでは、Javaのパフォーマンスチューニングを扱います。
19. パフォーマンス
Javaのパフォーマンスチューニングは 「JITを理解し、GCを選び、メモリを見る」 の3点。プロファイリング・ベンチマーク・JIT・メモリの順で整理します。
19-1. プロファイリング(JFR / JMC / async-profiler)
Java Flight Recorder(JFR)
JDKに標準で組み込まれているプロファイラ。本番でも使える低オーバーヘッドが特徴。
java -XX:StartFlightRecording=duration=60s,filename=app.jfr -jar app.jar
Java Mission Control(JMC)
JFRの出力を可視化するツール。GC・スレッド・メソッド頻度などをグラフィカルに分析できる。
async-profiler
サードパーティ製の 超軽量プロファイラ。フレームグラフ生成が得意。
./profiler.sh -d 30 -f flame.html <pid>
「まずJFR、深く見るならasync-profiler」が現代のセオリー。
19-2. JMH(マイクロベンチマーク)
Java Microbenchmark Harness(JMH) はOpenJDK公式のマイクロベンチマークツール。JITのウォームアップなどを正しく扱ってくれます。
@Benchmark
public int testStringConcat() {
String s = "";
for (int i = 0; i < 100; i++) s += i;
return s.length();
}
@Benchmark
public int testStringBuilder() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) sb.append(i);
return sb.length();
}
Benchmark Mode Cnt Score Error Units
testStringConcat thrpt 25 0.012 ± 0.001 ops/us
testStringBuilder thrpt 25 2.341 ± 0.124 ops/us
「自分で System.nanoTime() で測るとJIT最適化に騙される」ので、マイクロベンチマークにはJMH必須。
19-3. JITのチューニング
JITの段階
インタプリタ実行 → C1コンパイル → C2コンパイル
(軽量、起動速度)(重量、ピーク性能)
JITのウォームアップは数千〜数万回のメソッド実行が必要。短命なバッチではJITが間に合わないので、AOT(GraalVM Native Image)が選択肢に。
JITのフラグ
java -XX:+TieredCompilation # 階層化(デフォルト)
java -XX:CompileThreshold=10000 # JIT起動のしきい値
java -XX:+PrintCompilation # JITのログ
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining # インライン化ログ
19-4. メモリ最適化
ヒープサイズの設定
java -Xms512m -Xmx4g app.jar
-Xms(初期)と -Xmx(最大)を 同じ値にすると安定することが多い(メモリ拡張のオーバーヘッド回避)。
よくあるメモリ問題
1. OutOfMemoryError: Java heap spaceヒープ枯渇
2. OutOfMemoryError: Metaspaceメタスペース枯渇(クラスローディング多発)
3. OutOfMemoryError: GC overhead GCで全然進まない
4. メモリリーク 参照が残り続ける(特に静的フィールド)
ヒープダンプ
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof app.jar
# 実行中のプロセスから取る
jcmd <pid> GC.heap_dump /tmp/heap.hprof
ダンプを VisualVM や Eclipse MAT で開いて分析。
よくあるリーク
staticフィールドへの蓄積- クロージャがインスタンスを保持
- ThreadLocalの解放忘れ
- ListenerやCallbackの登録解除忘れ
19-5. このセクションのまとめ
プロファイリング:
JFR: 標準・本番投入可
JMC: JFRの可視化
async-profiler: 軽量・フレームグラフ
ベンチマーク:
JMH(マイクロベンチマーク必須)
自前のnanoTimeはJITに騙される
JIT:
C1(軽量)→ C2(最適化)の階層
ウォームアップ後にピーク性能
短命バッチにはGraalVM Native Image
メモリ:
-Xmsと -Xmxを同値で安定
ヒープダンプ(HeapDumpOnOutOfMemoryError)
リーク源: static / ThreadLocal / Listener
次はJava 8〜21の進化総覧、FAQ、図解、ロードマップ、用語集を一気にまとめます。
20. Java 8〜21の進化
ここ10年のJavaの主要追加機能をバージョン別に整理します。
Java 8(2014) ─ 関数型革命
- ラムダ式 / メソッド参照
- Streams API
- Optional
- デフォルトメソッド
- 新しい日時API(java.time)
「Javaを再定義したLTS」。今でも本番で広く使われている。
Java 9(2017)
- モジュールシステム(Project Jigsaw)
- JShell(REPL)
- Collectionのof() ファクトリ
- interfaceにprivateメソッド
Java 10(2018)
Java 11(2018) LTS
- HTTP Client(
java.net.http) varをラムダの引数に- Filesの便利メソッド追加
長く使われたLTS。Java 8からの移行先として人気でした。
Java 12〜13(2019)
- switch式(プレビュー)
- テキストブロック(プレビュー)
Java 14(2020)
- switch式(正式)
- record(プレビュー)
- より良いNullPointerExceptionメッセージ
Java 15(2020)
- テキストブロック(正式)
- sealedクラス(プレビュー)
Java 16(2021)
- record(正式)
- パターンマッチングfor instanceof
- Stream.toList()
Java 17(2021) LTS
- sealedクラス(正式)
- パターンマッチングfor switch(プレビュー)
Java 17 LTSは モダンJavaの事実上の標準として多くの企業に採用されました。
Java 18〜20(2022〜2023)
- Virtual Threads(プレビュー)
- Structured Concurrency(インキュベータ)
- Pattern Matching for switch(プレビュー強化)
Java 21(2023) LTS
- Virtual Threads(正式)
- Pattern Matching for switch(正式)
- Sequenced Collections
- Generational ZGC
現代の最新LTS。Spring Boot 3.2+ が対応し、本番投入が広がっています。
Java 22〜23(2024)
- Stream Gatherers(中間操作の汎化)
- Statements before super()(コンストラクタの柔軟化)
- Foreign Function & Memory API(Cとの相互運用)
21. よくある落とし穴FAQ
実務で頻発するJavaの落とし穴を一問一答で。
Q1. == と equals どちらを使う?
プリミティブは ==。参照型は equals(特にString、Integerなどのラッパー)。
Q2. Integer の == で100だとtrue、200だとfalseの理由は?
Integer Cache(-128〜127)のためです。equals を使えば常に値比較されます。
Q3. String s1 = "a"; String s2 = "a"; s1 == s2 は?
true(String Poolで共有される)。だが s2 = new String("a") だと別オブジェクトでfalse。比較は equals で。
Q4. Listのループ中でremoveするとなぜConcurrentModificationException?
イテレーション中に直接コレクションを変更すると例外。Iterator.remove() かStreamのfilterを使う。
Q5. Optional<User> を引数にしてはいけないのはなぜ?
Optionalは戻り値専用に設計されている。引数なら null 可能 + nullチェックで十分。
Q6. static を使いすぎると何が悪い?
テスト困難(モック不可)、スレッド安全性が悪い、グローバル状態。ユーティリティと定数以外は控える。
Q7. synchronized と volatile の違いは?
synchronized: アトミック性 + 可視性。volatile: 可視性のみ。インクリメントには synchronized か AtomicInteger。
Q8. checked例外とunchecked例外、どちらにすべき?
近年は unchecked寄り。フレームワーク(Springなど)もRuntimeExceptionを投げる方針。
Q9. Stream を再利用できないと聞いた
その通り。stream.count() の後にもう一度使うと例外。毎回 list.stream() で作り直す。
Q10. parallelStreamは速い?
CPUバウンド + 大量要素 + 副作用なしのときだけ速い。それ以外はむしろ遅い。
Q11. try-with-resources で複数のリソースは?
try (R1 r1 = ...; R2 r2 = ...) { ... } のようにセミコロンで区切る。closeは逆順。
Q12. record のフィールドを変更したい
recordはイミュータブル。新しいインスタンスを作る withX(...) メソッドを自分で書くか、別の record を返す。
Q13. NullPointerExceptionを防ぐ最善策は?
Optionalを使うObjects.requireNonNullで早期検出@NonNullアノテーション + 静的解析recordを使ってデフォルト値を強制
Q14. Virtual Threadsは何でも速くなる?
I/Oバウンド向け。CPUバウンドには効果なし。synchronized でピン留めされる罠もあるので注意(Java 21後期で改善中)。
Q15. var はどこまで使うべき?
右辺で型が明らかなときだけ。var x = process() のように戻り値の型が読み手に伝わらない場面では避ける。
Q16. equals をオーバーライドしたら必ず hashCode も?
そう。HashMap/HashSetが壊れる。逆も然り。Objects.hash(...) で簡単に書ける。
Q17. なぜ String はfinal?
セキュリティ(再定義を防ぐ)、性能(文字列プール)、スレッド安全性。Integerなど他のラッパーもfinal。
Q18. interfaceとabstract class、どっち?
まずinterface(複数実装可、契約)。状態や共通実装が要ればabstract class。Java 8+ のdefaultメソッドでinterfaceの表現力が上がった。
Q19. ジェネリクスの List<Integer> と int[]、どっち?
APIならList、性能ならプリミティブ配列。Streamの IntStream でプリミティブ専用Streamもある。
Q20. Stream.collect(Collectors.toList()) と Stream.toList()
Java 16+ では toList() が推奨。不変リストを返す。Collectors.toList() は ArrayList を返す(変更可)。
22. 図解: メモリモデル / クラスローディング / GC
文字だけでは伝わりにくいJavaの挙動を、図で整理します。
22-1. JVMのメモリ領域
┌──────────────────────────────────────┐
│ JVM Memory │
│ │
│ ┌─────────┐ ┌─────────────────┐ │
│ │ Stack │ │ Heap │ │
│ │ (各ス │ │ │ │
│ │ レッド)│ │ ┌─────────────┐ │ │
│ │ │ │ │ Young Gen │ │ │
│ │ ローカル│ │ │ ├ Eden │ │ │
│ │ 変数・ │ │ │ ├ Survivor 0 │ │ │
│ │ プリミ │ │ │ └ Survivor 1 │ │ │
│ │ ティブ │ │ ├─────────────┤ │ │
│ │ 値・参照│ │ │ Old Gen │ │ │
│ │ │ │ └─────────────┘ │ │
│ └─────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Metaspace(クラス情報、メソッド)│ │
│ └─────────────────────────────────┘ │
└──────────────────────────────────────┘
オブジェクトはHeap、参照はStack
プリミティブはほぼStack
クラス情報はMetaspace
22-2. クラスローダー階層
Bootstrap ClassLoader
(java.lang.* 等の中核)
↓
Platform ClassLoader
(java.sql, java.xml等)
↓
Application ClassLoader
(ユーザコード、CLASSPATH上)
↓
(ユーザ定義ClassLoader、
例: Webコンテナ)
Parent Delegation Model:
クラスを探すとき、まず親に問い合わせる
→ java.lang.Stringを上書きできない(安全)
22-3. 世代別GCの動き
新規オブジェクト
↓
Eden ─────→ 満杯 ─→ Minor GC
↓
生き残り → Survivor 0
↓
Minor GC
↓
Survivor 0 → Survivor 1
↓
何度も生き残るとOldに昇格
↓
Old Gen
↓
Major GC(停止時間が長い)
「ほとんどのオブジェクトはすぐ死ぬ」という仮説に基づく
22-4. JITコンパイル
ソース → javac → バイトコード(.class)
↓
JVMがインタプリタとして実行
↓
実行回数のしきい値超え
↓
C1コンパイラ
(軽量最適化、起動速度)
↓
さらに頻繁に呼ばれる
↓
C2コンパイラ
(重量最適化、ピーク性能)
↓
機械語
22-5. Virtual Threadsの動き
Virtual Thread 1 ─┐
Virtual Thread 2 ─┼─→ Carrier Thread (OS)
Virtual Thread 3 ─┘ │
│ I/O待ち
↓ アンマウント
別のVirtual Threadが
同じCarrierを使う
数百万のVirtual Threadが
数十のOSスレッドで動く
23. 学習ロードマップ(30日)
体系的にJavaを学ぶための30日プラン。1日1〜2時間を想定。
Week 1(基礎)
- Day 1: 環境構築(JDK 21)、Hello World、IDE(IntelliJ IDEA)
- Day 2: プリミティブ型と参照型、変数、演算子
- Day 3: 制御フロー(if、for、switch式)
- Day 4: メソッド・引数・戻り値・var
- Day 5: 配列とコレクション基礎(List/Map)
- Day 6: Stringと文字列操作
- Day 7: 簡単なCLIプログラム
Week 2(OOP)
- Day 8: クラス・this・コンストラクタ
- Day 9: アクセス修飾子・static・final
- Day 10: 継承・super・@Override
- Day 11: interface・defaultメソッド
- Day 12: abstract class・多態性
- Day 13: equals / hashCode / toString
- Day 14: recordとsealedの基礎
Week 3(モダンJava)
- Day 15: ジェネリクス・ワイルドカード・PECS
- Day 16: ラムダ式・関数型インターフェース
- Day 17: Streams API
- Day 18: Optionalとnull安全
- Day 19: パターンマッチングfor switch
- Day 20: 例外処理・try-with-resources
- Day 21: 復習・小規模アプリ
Week 4(実践)
- Day 22: ファイルI/O・java.nio.file
- Day 23: MavenまたはGradle
- Day 24: JUnit 5 + AssertJ + Mockito
- Day 25: 並行処理(Thread / ExecutorService)
- Day 26: CompletableFuture
- Day 27: Virtual Threads
- Day 28: モジュールシステム入門
- Day 29: Spring Bootで簡単なREST API
- Day 30: 全体振り返り、好きなOSSのソースを読む
30日後の到達目標
- Java 21の主要な構文を読み書きできる
- record / sealed / パターンマッチングで関数型風に書ける
- Streamsで宣言的にコレクション処理を書ける
- JUnit 5 + Mockitoでテストを書ける
- MavenかGradleでプロジェクトを管理できる
- Virtual Threadsの威力を理解している
- Spring Bootで簡単なWeb APIを作れる
24. 用語集
本ガイドに登場した主要な用語を50音順・アルファベット順で。
あ行
- アクセサ: フィールドへのアクセスを公開するgetter/setter
- アノテーション:
@で書くメタデータ。コンパイラやランタイムが利用 - イミュータブル: 変更不可。
finalフィールド + setterなし - インターフェース: 振る舞いの契約。Java 8+ でdefaultメソッドも持てる
か行
- 可視性修飾子: private / package-private / protected / public
- ガベージコレクション(GC): 不要オブジェクトの自動回収
- クラスローダー: クラスをJVMにロードする仕組み
- 継承: extendsで親クラスを継承(単一継承)
- コンパイル: javacでソースをバイトコードに
さ行
- シールドクラス(sealed): permitsで子クラスを限定
- ジェネリクス: 型パラメータ化、型消去で実装
- 静的型付け: コンパイル時に型が決まる
た行
- 多態性: 同じインターフェースで異なる実装を扱える
- チェック例外: throwsで宣言、catchを強制
- 抽象クラス(abstract class): 一部実装を持つ継承用クラス
な行
- ナチュラルオーダー: Comparableで定義された自然順序
は行
- バイトコード: JVMが実行する中間表現
- パターンマッチング: switch / instanceofで構造的にマッチ
- プリミティブ型: int/long/double等、値型の8種
- プロビジョナルマッチング:
case Type tで型 + 束縛 - ボクシング: int → Integerの自動変換
ま行
- マルチキャッチ: catch (A | B e)
- メソッド参照: ClassName::methodの構文
- モジュール: module-info.javaを持つ独立した単位
A〜Z
- AOT(Ahead-Of-Time): 実行前にネイティブコード化(GraalVM Native Image)
- CompletableFuture: 非同期プログラミングの中核
- Future: 非同期計算の結果
- HotSpot: 公式JVMの名前
- JAR: Java Archive、jarファイル
- JDK / JRE / JVM: 開発キット / 実行環境 / 仮想マシン
- JEP: JDK Enhancement Proposal、機能提案
- JFR: Java Flight Recorder、プロファイラ
- JIT: Just-In-Time、実行時コンパイル
- JMH: Java Microbenchmark Harness、ベンチマークツール
- JVM: Java Virtual Machine
- MVP: Maven Versions Plugin
- OOP: Object-Oriented Programming
- POJO: Plain Old Java Object、純粋なJavaオブジェクト
- record: イミュータブルなデータクラス(Java 14+)
- SAM: Single Abstract Method、関数型インターフェース
- sealed: 継承を限定するキーワード(Java 17+)
- SPI: Service Provider Interface
- Streams: コレクションの宣言的処理
- Virtual Threads: JVM管理の超軽量スレッド(Java 21+)
発展: JVMと実務機能
ここからはJavaの各機能を 実例とともに深掘り。JVM、GC、Concurrency、Streams、Spring、Maven/Gradleまで実用に直結する内容を網羅。
25. JVM内部詳細
25-1. クラスローディング
1. Loading .classをMethod Areaにロード
2. Linking Verification + Preparation + Resolution
3. Initialization staticイニシャライザ実行
親委譲モデル
Bootstrap ClassLoader(JVM内部、java.lang.* 等)
↓ 委譲
Platform ClassLoader(JDK標準ライブラリ)
↓ 委譲
Application ClassLoader(CLASSPATHのクラス)
↓ 委譲
カスタムClassLoader(Webコンテナ等)
子は親に問い合わせてから自分が探す
→ java.lang.Stringを上書きできない
25-2. JITコンパイラの段階
インタプリタ
↓ 実行回数 > 1500
C1 (Client Compiler)
軽量最適化、profile収集
↓ さらにホット
C2 (Server Compiler)
重量最適化、ピーク性能
TieredCompilationでこの階層が自動
JITの最適化例
- Inlining: 関数呼び出しの展開
- Escape Analysis: スタック割り当て
- Loop Unrolling: ループ展開
- Method Specialization: 型特化
- Dead Code Elimination
- Common Subexpression Elimination
- Branch Prediction
- Inline Caching
-XX:+PrintCompilation でJITのログ。
25-3. メモリ領域
Heap:
Young Generation
Eden Space
Survivor 0
Survivor 1
Old Generation (Tenured)
Metaspace: クラス情報(Java 8+)
PermGen: 旧版(Java 7以前、Metaspaceに置換)
Stack: 各スレッドのローカル変数
Native Memory: JNI、Direct Buffer
25-4. GCの進化と選択
| GC | バージョン | 特徴 |
|---|---|---|
| Serial GC | 〜 | シングルスレッド |
| Parallel GC | Java 5+ | スループット重視 |
| CMS | Java 1.4〜14 | 低レイテンシ(廃止) |
| G1 GC | Java 9+ デフォルト | 領域分割、低レイテンシ |
| ZGC | Java 11+ | 超低レイテンシ(< 1ms) |
| Shenandoah | Java 12+ | Red Hat製、ZGCと類似 |
| Generational ZGC | Java 21+ | 世代別 + ZGCの組み合わせ |
| Epsilon GC | Java 11+ | 何もしないGC(測定用) |
java -XX:+UseG1GC ...
java -XX:+UseZGC ...
java -XX:+UseShenandoahGC ...
GCの選び方
スループット重視 + 大ヒープ: Parallel GC
低レイテンシ + 普通の規模: G1 GC(デフォルト、無難)
超低レイテンシ + 巨大ヒープ: ZGC
組み込み・小さい: Serial GC
25-5. JVMパラメータ
# ヒープサイズ
-Xms512m # 初期
-Xmx4g # 最大
-XX:NewRatio=2 # Old / Young比
# GC
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-Xlog:gc* # GCログ
# JIT
-XX:+TieredCompilation # 階層化(デフォルト)
-XX:+PrintCompilation # JITログ
-XX:CompileThreshold=10000
# OOM対策
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heap.hprof
-XX:+ExitOnOutOfMemoryError
# プロファイル
-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder
25-6. このセクションのまとめ
- 親委譲モデルで安全
- JITはC1 → C2の階層化
- ヒープはYoung + Old + Metaspace
- GCはG1(デフォルト)/ ZGC(低レイテンシ)
- -Xmsと -Xmxは同値で安定
- HeapDumpOnOutOfMemoryErrorは必須
26. Streams API完全版
26-1. Streamの3段階
Source: コレクション、配列、Stream.of(), Files.lines()
Operations: map, filter, sorted, distinct, peek, limit, skip
Terminal: collect, reduce, forEach, count, anyMatch, allMatch
List<String> result = users.stream() // Source
.filter(u -> u.getAge() >= 18) // Intermediate
.map(User::getName) // Intermediate
.sorted() // Intermediate
.collect(Collectors.toList()); // Terminal
26-2. Collectors完全版
import static java.util.stream.Collectors.*;
// リスト
List<T> list = stream.collect(toList());
List<T> immutable = stream.toList(); // Java 16+
// セット
Set<T> set = stream.collect(toSet());
Set<T> sorted = stream.collect(toCollection(TreeSet::new));
// マップ
Map<K, V> map = stream.collect(toMap(User::getId, Function.identity()));
// 重複キー処理
Map<K, V> map = stream.collect(toMap(
User::getId,
Function.identity(),
(existing, duplicate) -> existing // 衝突時は既存を保持
));
// グループ化
Map<K, List<V>> groups = stream.collect(groupingBy(User::getDepartment));
// グループ化 + 集約
Map<K, Long> counts = stream.collect(groupingBy(
User::getDepartment,
counting()
));
Map<K, Double> avgs = stream.collect(groupingBy(
User::getDepartment,
averagingInt(User::getSalary)
));
// パーティショニング(booleanでグループ化)
Map<Boolean, List<User>> adults = stream.collect(partitioningBy(
u -> u.getAge() >= 18
));
// 結合
String joined = stream.map(User::getName).collect(joining(", ", "[", "]"));
// 集計
IntSummaryStatistics stats = stream.collect(summarizingInt(User::getAge));
double avg = stats.getAverage();
int max = stats.getMax();
26-3. パフォーマンス考慮
- 短絡評価: findFirst / anyMatchは早期終了
- parallelStream: CPU並列、ただしオーバーヘッド大
- Boxed primitive: IntStream / LongStreamで回避
- 中間Stream: 累積しないので問題ない(遅延)
- collectの選択: toList vs Collectors.toList()(Java 16+ で前者)
26-4. 高度なStream
// flatMap
Stream<List<Integer>> nested = ...;
Stream<Integer> flat = nested.flatMap(List::stream);
// reduce
int sum = stream.reduce(0, Integer::sum);
Optional<Integer> max = stream.reduce(Integer::max);
// 並列
long count = list.parallelStream().filter(...).count();
// 無限Stream
Stream<Integer> naturals = Stream.iterate(1, i -> i + 1);
Stream<Double> randoms = Stream.generate(Math::random);
// Stream.takeWhile / dropWhile(Java 9+)
Stream.iterate(1, i -> i + 1).takeWhile(i -> i < 10).forEach(System.out::println);
26-5. このセクションのまとめ
- Source → Intermediate → Terminal
- Collectorsで柔軟な集約
- parallelStreamはCPUバウンドのみ
- IntStreamでprimitive最適化
- flatMap / reduce / takeWhileを活用
27. CompletableFuture深掘り
27-1. 構築
CompletableFuture<String> f1 = CompletableFuture.completedFuture("hello");
CompletableFuture<String> f2 = CompletableFuture.failedFuture(new RuntimeException());
CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> compute());
CompletableFuture<Void> f4 = CompletableFuture.runAsync(() -> doSomething());
27-2. 連鎖
CompletableFuture<Integer> result = CompletableFuture
.supplyAsync(() -> "hello")
.thenApply(String::length) // 同期変換
.thenApplyAsync(n -> n * 2) // 別スレッドで変換
.thenCompose(n -> heavyAsync(n)) // T -> CompletableFuture<U>
.thenCombine(otherFuture, (a, b) -> a + b) // 2つ待って結合
.exceptionally(e -> -1) // エラー時のフォールバック
.whenComplete((value, error) -> log(value, error));
27-3. 並行
// 全部待つ
CompletableFuture.allOf(f1, f2, f3).thenRun(() -> {
String r1 = f1.join();
String r2 = f2.join();
});
// 最初の1つ
CompletableFuture<Object> any = CompletableFuture.anyOf(f1, f2, f3);
// 結合
CompletableFuture<String> combined = f1.thenCombine(f2, (a, b) -> a + b);
27-4. ExecutorServiceと組み合わせ
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture<String> f = CompletableFuture.supplyAsync(
() -> compute(),
executor // 専用Executor
);
executor.shutdown();
デフォルトではForkJoinPool.commonPool() を使うが、I/Oバウンドなら専用プールが推奨。
27-5. このセクションのまとめ
- supplyAsync / runAsyncで起動
- thenApply / thenCompose / thenCombineで連鎖
- allOf / anyOfで並行
- exceptionally / whenCompleteでエラー
- 専用Executorで隔離
28. Virtual Threads(Java 21+)詳細
28-1. なぜVirtual Threads
従来:
OSスレッド = 1 MBのスタック、コンテキストスイッチが重い
数千が限界
非同期化(CompletableFuture / Reactive)が必要
Virtual Threads:
数百万本可能
「同期コードのまま」高並行
Project Loomの成果(Java 21で正式化)
28-2. 使い方
// 単独起動
Thread.ofVirtual().start(() -> {
System.out.println("hi");
});
// ExecutorServiceとして
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
HttpClient.newHttpClient().send(request, ...);
});
}
} // 1万本のVirtual Threadが並行
28-3. Carrier ThreadとMounting
Virtual Threadが 多数
↓ mount
Carrier Thread (OSスレッド、少数、ForkJoinPool)
↓
CPUコア
I/O待ち → unmount → 別VTが同じCarrierを使う
28-4. 注意点
// Bad: synchronizedの中でI/Oするとpinされる(OSスレッドが解放されない)
synchronized (lock) {
httpCall(); // この間Carrier Threadが独占
}
// Good: ReentrantLockを使う
lock.lock();
try {
httpCall();
} finally {
lock.unlock();
}
synchronized でのピン留め問題はJava 21後期で改善が進んでいます。
28-5. ThreadLocalとのコスト
// 数百万のVirtual Thread × ThreadLocal = メモリ問題
// 代替: ScopedValue(Java 21 preview)
static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
ScopedValue.where(CURRENT_USER, user).run(() -> {
// CURRENT_USER.get() で参照
});
28-6. Structured Concurrency
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<User> userTask = scope.fork(() -> fetchUser(id));
Subtask<List<Order>> ordersTask = scope.fork(() -> fetchOrders(id));
scope.join(); // 全部完了を待つ
scope.throwIfFailed(); // 失敗があれば例外
User user = userTask.get();
List<Order> orders = ordersTask.get();
}
並行タスクの ライフサイクルをスコープで管理。失敗時に他もキャンセル。PythonのTaskGroup、RubyのAsyncと同じ思想。
28-7. このセクションのまとめ
- Virtual Threadsで同期スタイル + 高並行
- I/Oバウンドで効果絶大
- synchronizedでピン留めに注意 → ReentrantLock
- ThreadLocalは乱用注意 → ScopedValue
- Structured Concurrencyでライフサイクル管理
29. Spring Boot入門
Java業務開発の事実上の標準フレームワーク。
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
}
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserRepository repo;
public UserController(UserRepository repo) {
this.repo = repo;
}
@GetMapping("/{id}")
public User get(@PathVariable Long id) {
return repo.findById(id).orElseThrow();
}
@PostMapping
public User create(@RequestBody @Valid CreateUserDto dto) {
return repo.save(new User(dto.name(), dto.email()));
}
}
@Entity
public class User {
@Id @GeneratedValue
private Long id;
private String name;
private String email;
// getters / setters / constructors
}
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
これだけで本格的なREST API。Spring Bootは世界で最も使われているJavaフレームワーク。
Springの主要モジュール
Spring Framework: コア、DI、AOP
Spring Boot: 自動設定、starter
Spring MVC / WebFlux: Web
Spring Data JPA / R2DBC: DB
Spring Security: 認証・認可
Spring Cloud: マイクロサービス
Spring Batch: バッチ処理
30. MavenとGradle詳細
30-1. Mavenの依存
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
dependencyManagement で BOM(Bill of Materials) をimportするとバージョン管理が楽。
30-2. Gradleの依存
plugins {
id("org.springframework.boot") version "3.2.0"
id("io.spring.dependency-management") version "1.1.4"
java
}
group = "com.example"
version = "1.0.0"
java.sourceCompatibility = JavaVersion.VERSION_21
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.test {
useJUnitPlatform()
}
30-3. 依存スコープ
Maven:
compile (default): すべてで利用可能
provided: コンパイルのみ(実行環境が提供)
runtime: 実行・テストのみ
test: テストのみ
system: ローカルパス(非推奨)
Gradle:
implementation: 公開しない依存
api: 公開する依存(推移的)
compileOnly: コンパイルのみ
runtimeOnly: 実行のみ
testImplementation: テスト用
30-4. このセクションのまとめ
- Maven: XML、規約重視
- Gradle: DSL、柔軟、Android標準
- BOMで依存バージョン統一
- 適切なスコープでビルド最適化
31. テスト戦略詳細
31-1. JUnit 5完全版
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayName("ユーザサービスのテスト")
class UserServiceTest {
private UserService service;
@BeforeAll
static void setupAll() { /* セッション全体 */ }
@BeforeEach
void setup() {
service = new UserService(new InMemoryRepo());
}
@AfterEach
void teardown() { /* 各テスト後 */ }
@Test
@Order(1)
@DisplayName("ユーザを正しく登録できる")
void shouldRegisterUser() {
var user = service.register("Alice", "a@b.com");
assertNotNull(user.getId());
}
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"0, 0, 0"
})
void addTest(int a, int b, int expected) {
assertEquals(expected, a + b);
}
@Nested
@DisplayName("バリデーションエラーの場合")
class WhenInvalid {
@Test
void shouldRejectEmptyName() {
assertThrows(IllegalArgumentException.class,
() -> service.register("", "a@b.com"));
}
}
@RepeatedTest(10)
void random_shouldBePositive() {
assertTrue(Math.random() >= 0);
}
@Test
@Timeout(5)
void shouldFinishIn5Sec() throws Exception {
Thread.sleep(1000);
}
@Tag("slow")
@Test
void slowTest() { /* 普段はスキップ */ }
}
31-2. AssertJ
import static org.assertj.core.api.Assertions.*;
assertThat(user)
.isNotNull()
.hasFieldOrPropertyWithValue("name", "Alice")
.extracting(User::getAge).isEqualTo(30);
assertThat(list)
.hasSize(3)
.containsExactly(1, 2, 3)
.doesNotContain(99)
.allMatch(x -> x > 0);
assertThatThrownBy(() -> service.register("", ""))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("invalid");
assertThatCode(() -> service.register("a", "b@c"))
.doesNotThrowAnyException();
JUnit 5のアサーションより 圧倒的に読みやすい。現代のデファクト。
31-3. Mockito
@ExtendWith(MockitoExtension.class)
class ServiceTest {
@Mock UserRepository repo;
@InjectMocks UserService service;
@Test
void test() {
when(repo.findById(1L)).thenReturn(Optional.of(new User("Alice")));
var user = service.getUser(1L);
assertThat(user.getName()).isEqualTo("Alice");
verify(repo).findById(1L);
verify(repo, never()).save(any());
}
}
31-4. Testcontainers
@Testcontainers
class IntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
.withDatabaseName("test")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configure(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
}
@Test
void test() {
// 本物のPostgreSQLに対してテスト
}
}
「本物のDB / Redis / Kafkaをコンテナで起動」してテスト。CIでも動く。
31-5. このセクションのまとめ
- JUnit 5: @Test / @ParameterizedTest / @Nested
- AssertJ: 流暢なアサーション
- Mockito: モック
- Testcontainers: 実DB / 実Redisでテスト
32. パフォーマンスチューニング
32-1. JFR / JMC
# プロファイル取得
java -XX:StartFlightRecording=duration=60s,filename=app.jfr -jar app.jar
# JMCで可視化
jmc app.jfr
JDK標準のプロファイラ。本番で使える低オーバーヘッド。
32-2. JMH
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class StringConcat {
String[] words;
@Setup
public void setup() {
words = new String[100];
for (int i = 0; i < 100; i++) words[i] = "word" + i;
}
@Benchmark
public String plus() {
String s = "";
for (var w : words) s += w;
return s;
}
@Benchmark
public String builder() {
var sb = new StringBuilder();
for (var w : words) sb.append(w);
return sb.toString();
}
}
mvn clean install
java -jar target/benchmarks.jar
JITウォームアップ込みで正確な計測。Stopwatch などの自前計測は信頼できない。
32-3. async-profiler
# 軽量サンプリング
./profiler.sh -d 30 -f flame.html <pid>
CPU・メモリ・ロックなどフレームグラフで可視化。JFRより軽量で本番向き。
32-4. このセクションのまとめ
- JFR + JMC(標準)
- JMH(マイクロベンチマーク必須)
- async-profiler(軽量・本番)
- 計測してから最適化
33. 周辺ライブラリ
33-1. ロギング
SLF4J: 抽象化(Facade)
Logback: 実装、Spring Boot標準
Log4j2: 実装、Apache製
java.util.logging: 標準(あまり使われない)
private static final Logger log = LoggerFactory.getLogger(MyClass.class);
log.info("user signed in: id={}, ip={}", user.id(), ip);
log.error("failed", exception);
{} プレースホルダで遅延フォーマット(パフォーマンス)。
33-2. Jackson(JSON)
ObjectMapper mapper = new ObjectMapper();
// シリアライズ
String json = mapper.writeValueAsString(user);
// デシリアライズ
User u = mapper.readValue(json, User.class);
// アノテーション
class User {
@JsonProperty("user_name")
String name;
@JsonIgnore
String password;
@JsonCreator
public User(@JsonProperty("name") String name) { ... }
}
// Tree model
JsonNode root = mapper.readTree(json);
String name = root.get("name").asText();
事実上の標準。Spring Bootに内蔵。
33-3. Lombok(賛否両論)
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class User {
private String name;
private int age;
}
// 自動生成: getters/setters/equals/hashCode/toString/builder
boilerplateを削減。ただしrecord / data classが登場した今、必要性は減少。
33-4. Guava
ImmutableList<String> list = ImmutableList.of("a", "b", "c");
ImmutableMap<String, Integer> map = ImmutableMap.of("a", 1, "b", 2);
// 文字列
String joined = Joiner.on(",").join(list);
List<String> parts = Splitter.on(",").splitToList(s);
// Optional
Optional<Integer> opt = Optional.of(42);
// Cache
Cache<String, User> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(10))
.build();
Google製。Java標準ライブラリの拡張集。多くの機能が標準ライブラリに取り込まれた。
33-5. このセクションのまとめ
- SLF4J + Logbackでロギング
- JacksonでJSON
- Lombokでboilerplate削減(recordで代替可)
- Guavaで標準ライブラリ拡張
34. デプロイとコンテナ
34-1. Dockerfile
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY pom.xml .
COPY src src
RUN ./mvnw package -DskipTests
FROM eclipse-temurin:21-jre-alpine
COPY --from=build /app/target/myapp.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
Multi-stage buildで最終イメージを小型化。
34-2. AOT(GraalVM Native Image)
# Spring Boot Native
./mvnw -Pnative native:compile
# 実行
./target/myapp
数msで起動、メモリも数MB。サーバレス・CLI向けに最適。ただし リフレクション制約あり。
34-3. JVMチューニングforクラウド
# コンテナ向け
java -XX:MaxRAMPercentage=75 -jar app.jar
# プロファイル
java -XX:+FlightRecorder -XX:StartFlightRecording=...
# GCログ
java -Xlog:gc*:file=gc.log:time,uptime:filecount=10,filesize=10M
34-4. このセクションのまとめ
- Multi-stage Dockerfile
- Native AOTで軽量化
- MaxRAMPercentageでコンテナ対応
- GCログを残す
35. Java拡張ロードマップ(60日)
Phase 1: 基礎(Day 1-15)
- 環境(JDK 21、IntelliJ)
- 型・var・nullable
- 制御フロー、switch式
- メソッド・コンストラクタ・final
- collections / String
Phase 2: OOP(Day 16-30)
- クラス・継承・interface
- abstract / sealed / record
- generics + ワイルドカード
- Optional / pattern matching
- 例外処理 / try-with-resources
Phase 3: モダン(Day 31-45)
- ラムダ + Streams
- CompletableFuture
- asyncストリーム(Project Reactor)
- Virtual Threads(Java 21)
- structured concurrency
Phase 4: 実践(Day 46-60)
- JUnit 5 + AssertJ + Mockito
- Spring BootでREST API
- Spring Data JPA
- Testcontainers
- JMHベンチマーク
- Native AOT公開
応用: Springと運用
37. Reactive Programming(Project Reactor)
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
Mono<User> user = userRepo.findById(1);
Flux<User> users = userRepo.findAll();
users.filter(u -> u.getAge() >= 18)
.map(User::getName)
.take(10)
.subscribe(System.out::println);
// 並行
Flux.range(1, 100)
.parallel(4)
.runOn(Schedulers.parallel())
.map(this::heavyComputation)
.sequential()
.subscribe();
// HTTP統合(Spring WebFlux)
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable Long id) {
return userRepo.findById(id);
}
ノンブロッキング・バックプレッシャ対応。Virtual Threadsが登場した今、必要性は再評価中。シンプルなasyncならVirtual Threads、複雑なストリーム処理ならReactor。
38. Hibernate / JPA詳細
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 100)
private String name;
@Column(unique = true, nullable = false)
private String email;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
}
// Repository(Spring Data JPA)
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
List<User> findByAgeGreaterThan(int age);
@Query("SELECT u FROM User u WHERE u.department.name = :dept")
List<User> findByDepartment(@Param("dept") String dept);
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.lastLogin < :date")
int deactivateInactive(@Param("date") LocalDateTime date);
}
N+1問題
// Bad
List<User> users = repo.findAll();
for (User u : users) {
u.getOrders().size(); // 各userで別クエリ!
}
// Good: JOIN FETCH
@Query("SELECT u FROM User u JOIN FETCH u.orders")
List<User> findAllWithOrders();
// または @EntityGraph
@EntityGraph(attributePaths = {"orders"})
List<User> findAll();
ORMの最大の罠。JPA利用時は必ず意識。
39. MicroservicesとSpring Cloud
@SpringBootApplication
@EnableDiscoveryClient // Eureka等のService Discovery
public class UserService {
public static void main(String[] args) {
SpringApplication.run(UserService.class, args);
}
}
// 他サービス呼び出し
@FeignClient(name = "order-service")
public interface OrderClient {
@GetMapping("/orders/{userId}")
List<Order> getOrders(@PathVariable Long userId);
}
// サーキットブレーカ
@RestController
public class UserController {
@Autowired private OrderClient orderClient;
@GetMapping("/users/{id}/orders")
@CircuitBreaker(name = "orders", fallbackMethod = "defaultOrders")
public List<Order> getOrders(@PathVariable Long id) {
return orderClient.getOrders(id);
}
public List<Order> defaultOrders(Long id, Throwable t) {
return Collections.emptyList();
}
}
Spring Cloud(Netflix OSSの継承)でマイクロサービスを構築。Eureka / Hystrix / Zuul / Ribbonなど。最近はKubernetes / Istioに移行する傾向。
40. ロギングと観測
40-1. SLF4J + Logback
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger - %msg%n</pattern>
</encoder>
</appender>
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<root level="INFO">
<appender-ref ref="JSON"/>
</root>
</configuration>
40-2. Micrometer + Prometheus
@Component
public class MyService {
private final MeterRegistry registry;
private final Counter requestCounter;
public MyService(MeterRegistry registry) {
this.registry = registry;
this.requestCounter = Counter.builder("requests")
.tag("service", "users")
.register(registry);
}
public void process() {
requestCounter.increment();
Timer.Sample sample = Timer.start();
try {
// 処理
} finally {
sample.stop(Timer.builder("processing.time").register(registry));
}
}
}
Prometheus / Datadog / New Relicと連携。
40-3. OpenTelemetry
Tracer tracer = openTelemetry.getTracer("my-service");
Span span = tracer.spanBuilder("operation").startSpan();
try (Scope scope = span.makeCurrent()) {
span.setAttribute("user.id", userId);
// 処理
} finally {
span.end();
}
分散トレーシング標準。
41. ベストプラクティス集
コード品質:
☐ -Xlint:all -Werrorで警告厳格
☐ Checkstyle / SpotBugs / SonarQubeをCIに
☐ Java 21 LTSを使う
☐ Nullable Reference Types相当として @Nullable / @NonNull
☐ Lombokよりrecordを優先
アーキテクチャ:
☐ Clean Architecture / ヘキサゴナル
☐ Domain Layerは外部依存ゼロ
☐ Dependency Inversionでinterface
☐ DDDの集約境界を意識
性能:
☐ JFR + JMCで本番プロファイル
☐ JMHでベンチマーク
☐ G1 GCまたはZGC
☐ -XX:MaxRAMPercentage=75(コンテナ)
☐ 必要ならNative AOT
セキュリティ:
☐ Spring Securityで認証・認可
☐ Bean Validationで入力検証
☐ OWASP Top 10を意識
☐ 依存ライブラリの脆弱性スキャン
42. Java周辺ツール
IDE:
IntelliJ IDEA(最強)
Eclipse(無料、巨大プラグイン)
VSCode + Extension Pack for Java
NetBeans
ビルド:
Maven / Gradle
Bazel(Google製、巨大プロジェクト)
テスト:
JUnit 5
TestNG
AssertJ / Hamcrest
Mockito / EasyMock
Testcontainers
Awaitility(asyncテスト)
ロギング:
SLF4J + Logback / Log4j2
Logstash encoder(JSON)
DI:
Spring(最大)
Guice(Google製、軽量)
Dagger(Android標準)
Quarkus / Micronaut(GraalVM親和)
ORM:
Hibernate / JPA
MyBatis(SQL直接)
jOOQ(type-safe SQL DSL)
Web:
Spring MVC / WebFlux
Quarkus
Micronaut
Helidon
メッセージング:
Apache Kafka
RabbitMQ
ActiveMQ
監視:
Micrometer
Prometheus
Grafana
ELK stack
43. Javaエコシステムの規模
Maven Central: 数百万のアーティファクト
Springエコシステム: 数百万人の開発者
GitHub: Javaは人気言語のトップ5
StackOverflow: Javaの質問は最多級
求人: 業界最大級
「就職に困らない言語」のひとつ。学習投資の元が取れる。
Javaのエコシステムの大きさは、単にライブラリ数が多いという意味ではない。長期保守、後方互換、監視、セキュリティ、ビルド、テスト、クラウド運用まで、企業システムで必要になる周辺がそろっている。新しい技術を採用するときも、Spring、Maven Central、JVM監視ツール、既存の運用知識と接続しやすい。
一方で、選択肢が多いことは迷いやすさにもなる。Spring Boot、Quarkus、Micronaut、Hibernate、jOOQ、MyBatis、Gradle、Mavenを場当たり的に混ぜると、構成が重くなる。プロジェクトでは、標準スタックを決め、例外的な採用には理由を残すことが大切である。
44. Javaの基本の整理
Javaは 業界最大のエコシステムを持つ言語のひとつ。Spring、Hibernate、Kafka、Elasticsearch、Hadoop、Spark ─ あなたが触れるエンタープライズシステムの大半がJavaで動いています。
「冗長な言語」というイメージはJava 21で完全に過去のもの。var、record、sealed、pattern matching、Virtual Threads ─ Modern Javaは驚くほどモダンです。
Javaを学ぶ価値(再掲):
- 巨大エコシステム + 安定性
- JVM 30年の最適化
- Kotlin / Scalaへ自然に拡張可
- Android(Kotlin推奨だがJavaも動く)
- 業務開発のメインストリーム
- 求人が多い
public class FinalWelcome {
public static void main(String[] args) {
System.out.println("Hello, Java.");
System.out.println("30 years of evolution. Still leading.");
}
}
Javaの世界で、生産的な日々を過ごしてください。
JVM仕様の詳細とバージョン履歴
Java仮想機械(JVM)のバイトコード
Java源コードはJavaコンパイラ(javac)によってJVMバイトコードにコンパイルされます。バイトコードは独立したプラットフォームで実行可能です。
Java ソースコード (.java)
↓ javac コンパイラ
JVM バイトコード (.class)
↓ JIT コンパイル(実行時)
ネイティブマシンコード
主要なバイトコード命令(JVM命令セット):
- ロード・ストア: aload, astore, iload, istore
- 算術演算: iadd, isub, imul, idiv
- 制御フロー: if_icmpeq, goto, invokevirtual
- オブジェクト操作: new, putfield, getfield
- メソッド呼び出し: invokevirtual, invokespecial, invokestatic
// Java コード
public int add(int a, int b) {
return a + b;
}
// バイトコード(javap -c で表示)
public int add(int, int);
Code:
0: iload_1 // a をロード
1: iload_2 // b をロード
2: iadd // 加算
3: ireturn // 結果を返す
JVM仕様はJava SE 21まで進化し、各バージョンで新しい命令やメカニズムが追加されています。
JITコンパイルと最適化
JVMはバイトコードを実行時にネイティブマシンコードにコンパイル(JIT: Just-In-Time Compilation)します。ホットスポット(頻繁に実行されるコード)を検出して最適化します。
バイトコード実行
↓
ホットメソッド検出(カウンター >= しきい値)
↓
JITコンパイラ起動
↓
最適化:
- インライン化(呼び出し先を呼び出し元に展開)
- デッドコード削除
- エスケープ分析(ローカルスコープ内のオブジェクトはスタック割り当て)
C2コンパイラ(サーバーJVM)では、複雑な最適化を実施します。これにより、解釈実行(Interpreter)より100倍以上高速になります。
メモリ管理とガベッジコレクション(GC)
GCアルゴリズムの詳細
Java 21ではさまざまなGCアルゴリズムが利用可能です。
| GC | 特性 | 用途 |
|---|---|---|
| G1GC | 低遅延、予測可能 | 一般的なサーバーアプリ |
| ZGC | 超低遅延(<10ms) | 低遅延要件 |
| Shenandoah | 同時GC、低遅延 | 大規模ヒープ |
| Serial GC | シングルスレッド | 小さいアプリ・テスト |
// JVMオプションでGCを指定
// java -XX:+UseG1GC -Xmx4g MyApp
// java -XX:+UseZGC -Xmx8g MyApp
// java -XX:+UseShenandoahGC MyApp
// GC ログを出力
// java -Xlog:gc*:file=gc.log MyApp
GCアルゴリズムの選択は、アプリケーションの要件(レイテンシ vs スループット)に応じて行う必要があります。
ヒープの構造
Java 8以降、ヒープは次の領域に分割されます。
ヒープメモリ
├─ Young Generation (新生代)
│ ├─ Eden Space
│ ├─ Survivor Space 0
│ └─ Survivor Space 1
└─ Old Generation (老年代)
└─ [Tenured Objects]
[Method Area / Metaspace]
[Stack Memory]
オブジェクトはEdenで生成され、GCサイクルを生き残るとSurvivor Spaceに移動し、さらに生き残るとOld Generationに昇格(promotion)します。
// OutOfMemoryError を理解する
java -Xmx100m MyApp // 100MB制限
// ヒープダンプを取得
jmap -dump:live,format=b,file=heap.bin <pid>
jhat heap.bin // 分析用ツール
マルチスレッドと同期化
volatile キーワードと可視性
volatileは変数への読み書きを常にメインメモリ経由で行い、キャッシュをバイパスします。
class StopFlag {
private volatile boolean stopped = false;
public void stop() {
stopped = true; // 全スレッドから可視
}
public void run() {
while (!stopped) { // 毎回メインメモリから読む
// 処理
}
}
}
Java メモリモデル(JMM)では、volatileへの書き込みより前のメモリ操作は、volatileからの読み込みより後のメモリ操作より前に完了することが保証されます(Happens-Before関係)。
ReentrantLock と Condition
synchronized の代替として、ReentrantLock と Condition を使うことで、より細かい制御が可能になります。
public class BoundedBuffer<T> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public void put(T element) throws InterruptedException {
lock.lock();
try {
while (isFull()) {
notFull.await(); // フルまで待機
}
add(element);
notEmpty.signal(); // 待機中のスレッドを起動
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (isEmpty()) {
notEmpty.await();
}
T element = remove();
notFull.signal();
return element;
} finally {
lock.unlock();
}
}
}
ReentrantLockはfair lockオプション(lock = new ReentrantLock(true))でFIFO順序を保証できます。
java.util.concurrent フレームワーク
ExecutorService、BlockingQueue、CountDownLatch などを組み合わせることで、スレッド管理を簡潔に書けます。
ExecutorService executor = Executors.newFixedThreadPool(10);
// タスク投入
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
// 並列タスク
});
}
// 完了を待つ
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
Future
ストリームAPIと関数型プログラミング
ストリーム処理パイプライン
Stream APIは遅延評価により、効率的なパイプライン処理を可能にします。
List<String> words = Arrays.asList("apple", "banana", "avocado");
String result = words.stream()
.filter(w -> w.startsWith("a")) // [apple, avocado]
.map(String::toUpperCase) // [APPLE, AVOCADO]
.sorted() // [APPLE, AVOCADO]
.collect(Collectors.joining(", ")); // "APPLE, AVOCADO"
System.out.println(result);
中間操作(filter, map, sorted)は遅延評価され、終端操作(collect, forEach)で初めて実行されます。
Collector のカスタマイズ
Collectors ユーティリティクラスは様々な集約操作を提供します。
List<Person> people = /* ... */;
// グループ化
Map<String, List<Person>> byCity = people.stream()
.collect(Collectors.groupingBy(Person::getCity));
// パーティション化
Map<Boolean, List<Person>> byAge = people.stream()
.collect(Collectors.partitioningBy(p -> p.getAge() >= 18));
// カスタムCollector
Collector<Integer, ?, Integer> summing =
Collector.of(
() -> new int[1], // supplier
(acc, n) -> acc[0] += n, // accumulator
(a, b) -> { a[0] += b[0]; return a; } // combiner
);
int total = IntStream.range(1, 11)
.boxed()
.collect(summing); // 55
並列ストリーム(parallelStream)を使うことで、マルチスレッド処理が自動化されます。ただし並列化のオーバーヘッドが大きい場合は逆効果になります。
まとめ
Javaは、静的型付け、JVM、豊富な標準ライブラリ、成熟したエコシステムを背景に、大規模で長期運用されるシステムに強い言語です。クラス設計、例外、並行処理、ビルド、テスト、フレームワークを一体で理解すると、読みやすく変更しやすいJavaコードを書きやすくなります。