Java

Javaの概要と歴史

Javaとは何か

Javaは、1995年にSun Microsystems(現Oracle Corporation)によって発表された、オブジェクト指向プログラミング言語である。Javaの最大の特徴は「Write Once, Run Anywhere(WORA)」という設計哲学にある。これは、一度書いたJavaプログラムが、Java Virtual Machine(JVM)が動作するあらゆるプラットフォーム上で、再コンパイルなしに実行できることを意味する。

Javaは、C++の複雑さを排除しつつ、堅牢性・セキュリティ・移植性を重視して設計された。手動メモリ管理(ポインタ操作)を排除し、ガベージコレクションによる自動メモリ管理を採用したことで、メモリリークやバッファオーバーフローといった問題を大幅に軽減した。

Javaの主要な設計原則は以下の通りである。

  • シンプルさ: C++から複雑な機能(多重継承、ポインタ演算、演算子オーバーロード)を排除
  • オブジェクト指向: すべてがオブジェクト(プリミティブ型を除く)として扱われる
  • プラットフォーム非依存: バイトコードとJVMにより、OS・ハードウェアに依存しない
  • 堅牢性: 強い型付け、例外処理、ガベージコレクションによる安定性
  • セキュリティ: サンドボックスモデル、バイトコード検証による安全な実行環境
  • マルチスレッド対応: 言語レベルでの並行処理サポート
  • 高パフォーマンス: JIT(Just-In-Time)コンパイラによるネイティブコードへの変換

Javaの歴史

誕生(1991年〜1995年)

Javaの起源は、1991年にSun MicrosystemsのJames Gosling率いる「Green Team」が開始した「Green Project」に遡る。当初は、家電製品向けのプログラミング言語「Oak」として開発された。Oakという名前はGoslingのオフィスの窓から見えたオークの木に由来するが、商標の問題からJavaに改名された。Javaという名前は、チームメンバーが好んだJava島産のコーヒーにちなんでいる。

1995年5月23日、Sun MicrosystemsはSunWorld'95カンファレンスでJava 1.0を正式に発表した。同時に、Netscape Navigator 2.0にJavaアプレットのサポートが追加され、Webブラウザ上でJavaプログラムを実行できるようになった。

バージョンの変遷

バージョンリリース年主な追加機能
Java 1.01996年初版リリース、AWT、アプレット
Java 1.11997年内部クラス、JavaBeans、JDBC、RMI
Java 1.2 (J2SE)1998年Swingフレームワーク、Collections Framework、JIT コンパイラ
Java 1.32000年HotSpot JVM、JNDI、Java Sound API
Java 1.42002年NIO、正規表現、assert文、XML処理
Java 5 (1.5)2004年ジェネリクス、アノテーション、列挙型、拡張for文、可変長引数、オートボクシング
Java 62006年スクリプトエンジン、JDBC 4.0、Java Compiler API
Java 72011年try-with-resources、ダイヤモンド演算子、switch文の文字列対応、NIO.2、Fork/Join
Java 82014年ラムダ式、Stream API、Optional、Date and Time API (java.time)、デフォルトメソッド
Java 92017年モジュールシステム (Jigsaw)、JShell、リアクティブストリーム
Java 102018年ローカル変数型推論 (var)、G1GCの改善
Java 11 (LTS)2018年HTTP Client API、var in lambda、String新メソッド
Java 12-162019-2021年Switch式、Text Blocks、Records、Sealed Classes(プレビュー)
Java 17 (LTS)2021年Sealed Classes正式版、Pattern Matching for instanceof
Java 18-202022-2023年Virtual Threads(プレビュー)、Structured Concurrency(プレビュー)
Java 21 (LTS)2023年Virtual Threads正式版、Pattern Matching for switch、Sequenced Collections

Java 9以降、Oracleは6ヶ月ごとの新バージョンリリースサイクルを採用した。LTS(Long-Term Support)バージョンは、Java 11、17、21と3年ごとにリリースされ、企業での採用に適した長期サポートが提供される。

Javaのエディション

Javaは用途別に3つのエディションに分かれている。

Java SE(Standard Edition)

Java SEは、Javaプラットフォームの基盤となるエディションである。コア言語機能、基本API(コレクション、I/O、ネットワーキング、マルチスレッドなど)を含む。デスクトップアプリケーション、コマンドラインツール、基本的なサーバーアプリケーションの開発に使用される。

Java EE / Jakarta EE(Enterprise Edition)

Java EEは、Java SEを拡張し、大規模なエンタープライズアプリケーション開発に必要な仕様群を提供するエディションである。Servlet、JSP、EJB、JPA、CDI、JAX-RSなどの仕様を含む。2017年にOracleからEclipse Foundationに移管され、Jakarta EEとして再出発した。名前空間もjavaxからjakartaに変更された。

Java ME(Micro Edition)

Java MEは、組み込みデバイスやモバイルデバイス向けの軽量エディションである。リソースが限られた環境での動作を前提に設計されている。かつてはフィーチャーフォンのアプリケーション開発で広く使われていたが、スマートフォンの普及(特にAndroidはJava SEベース)により、利用は減少している。

Javaエコシステムの全体像

Javaのエコシステムは、プログラミング言語の中でも最も成熟し、広範なものの一つである。

JVM言語

JVM上ではJava以外にも多くの言語が動作する。

  • Kotlin: JetBrainsが開発。Androidの公式開発言語。Javaとの100%互換性
  • Scala: オブジェクト指向と関数型プログラミングの融合。Apache Sparkの開発言語
  • Groovy: 動的型付けのJVM言語。Gradleビルドスクリプトに使用
  • Clojure: JVM上のLisp方言。イミュータブルデータ構造を重視

主要フレームワーク

  • Spring Framework / Spring Boot: エンタープライズJava開発のデファクトスタンダード
  • Quarkus: クラウドネイティブ向けの軽量Javaフレームワーク
  • Micronaut: マイクロサービス向けフレームワーク。コンパイル時DI
  • Jakarta EE: 標準仕様に基づくエンタープライズフレームワーク

ビルドツール

  • Maven: XMLベースのビルドツール。最も広く使われている
  • Gradle: GroovyまたはKotlinスクリプトベースのビルドツール。柔軟性が高い

IDE

  • IntelliJ IDEA: JetBrains製。最も人気のあるJava IDE
  • Eclipse: オープンソースIDE。プラグインエコシステムが豊富
  • Visual Studio Code: Microsoftのエディタ。Java Extension Packで対応

その他の重要なツール・ライブラリ

  • Lombok: ボイラープレートコード削減
  • MapStruct: オブジェクトマッピング
  • Jackson / Gson: JSON処理
  • Apache Commons: 汎用ユーティリティライブラリ群
  • Log4j2 / SLF4J + Logback: ロギングフレームワーク

Javaは2024年現在も、TIOBE Indexで常にトップ3〜4に位置し、世界中の企業システム、Webアプリケーション、Androidアプリ、ビッグデータ処理、IoTなど、あらゆる分野で活用されている。Stack Overflow Developer Survey 2023によると、プロフェッショナル開発者の約30%がJavaを使用しており、その安定性と信頼性は30年近い歴史が証明している。

JVMアーキテクチャ

JVM(Java Virtual Machine)の仕組み

JVM(Java Virtual Machine)は、Javaプログラムを実行するための仮想マシンである。Javaソースコード(.java)はJavaコンパイラ(javac)によってバイトコード(.class)にコンパイルされ、JVMがこのバイトコードを各プラットフォームのネイティブコードに変換して実行する。これにより、「Write Once, Run Anywhere」が実現される。

JVMの実行フローは以下の通りである。

Javaソースコード (.java)
    ↓ javacコンパイラ
バイトコード (.class)
    ↓ クラスローダー
JVMメモリにロード
    ↓ バイトコード検証
    ↓ インタープリタ / JITコンパイラ
ネイティブコード実行

JVMの主要な実装としては、以下がある。

  • Oracle HotSpot: 最も広く使われているJVM実装
  • OpenJ9 (Eclipse): IBMが開発。メモリ効率に優れる
  • GraalVM: Oracle Labsが開発。多言語対応、AOTコンパイル対応
  • Azul Zing / Zulu: 商用 / オープンソースのJVM

クラスローダー(Class Loader)

クラスローダーは、Javaクラスファイルをメモリにロードする仕組みである。JVMには3つの階層的なクラスローダーがある。

Bootstrap Class Loader

JVMの起動時に最初にロードされるクラスローダーである。java.langjava.utilなどのJavaコアライブラリ(rt.jar、Java 9以降はモジュール)をロードする。C/C++で実装されており、Javaコードから直接参照できない。

Extension (Platform) Class Loader

Java SEのプラットフォーム拡張クラスをロードする。$JAVA_HOME/lib/extディレクトリ(Java 8以前)またはプラットフォームモジュール(Java 9以降)からクラスをロードする。

Application (System) Class Loader

アプリケーションのクラスパス上にあるクラスをロードする。-classpathCLASSPATH環境変数で指定されたディレクトリやJARファイルからクラスを読み込む。開発者が作成したクラスは通常このローダーによってロードされる。

委譲モデル(Parent Delegation Model)

クラスローダーは「親委譲モデル」に基づいて動作する。クラスのロード要求を受けると、まず親クラスローダーに委譲し、親がロードできない場合にのみ自身でロードを試みる。

// カスタムクラスローダーの例
public class CustomClassLoader extends ClassLoader {
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException(name);
        }
        return defineClass(name, classData, 0, classData.length);
    }
    
    private byte[] loadClassData(String className) {
        String path = className.replace('.', File.separatorChar) + ".class";
        try (InputStream is = new FileInputStream(path)) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = is.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            return null;
        }
    }
}

メモリモデル

JVMのメモリ領域は、以下の5つの主要な領域に分かれている。

ヒープ(Heap)

すべてのオブジェクトインスタンスと配列が格納される領域である。ガベージコレクションの主な対象となる。ヒープはさらに以下のように分割される。

  • Young Generation(新世代領域)
    • Eden Space: 新しいオブジェクトが最初に割り当てられる領域
    • Survivor Space (S0, S1): Minor GCを生き延びたオブジェクトが移動する領域
  • Old Generation(旧世代領域): 長期間生存するオブジェクトが格納される領域
  • Metaspace(Java 8以降): クラスメタデータが格納される領域(旧PermGen)

スタック(Stack)

各スレッドに一つずつ割り当てられるメモリ領域である。メソッド呼び出しごとにスタックフレームが作成され、ローカル変数、オペランドスタック、フレームデータが格納される。LIFO(Last In, First Out)方式で管理される。

メソッド領域(Method Area)

クラス構造、メソッドデータ、コンストラクタ、フィールドデータ、ランタイム定数プールが格納される領域である。すべてのスレッドから共有される。

PCレジスタ(Program Counter Register)

各スレッドが現在実行中の命令のアドレスを保持するレジスタである。スレッドごとに独立して存在する。

ネイティブメソッドスタック

JNI(Java Native Interface)を通じて呼び出されるネイティブメソッドのためのスタック領域である。

JVMメモリ構造
┌─────────────────────────────────────────┐
│                ヒープ (Heap)              │
│  ┌─────────────────┬──────────────────┐ │
│  │  Young Generation │  Old Generation  │ │
│  │  ┌─────┬───┬───┐│                  │ │
│  │  │Eden │S0 │S1 ││                  │ │
│  │  └─────┴───┴───┘│                  │ │
│  └─────────────────┴──────────────────┘ │
├─────────────────────────────────────────┤
│          Metaspace (Off-Heap)            │
├─────────────────────────────────────────┤
│  スタック  │  スタック  │  スタック      │
│ (Thread 1)│ (Thread 2)│ (Thread 3)     │
├─────────────────────────────────────────┤
│         Method Area / 定数プール         │
├─────────────────────────────────────────┤
│        PC Register (各スレッド)          │
├─────────────────────────────────────────┤
│       Native Method Stack               │
└─────────────────────────────────────────┘

ガベージコレクション(GC)

ガベージコレクションは、使用されなくなったオブジェクトのメモリを自動的に回収する仕組みである。JVMには複数のGCアルゴリズムが用意されている。

Serial GC

単一スレッドでGCを実行する最もシンプルなコレクタ。クライアントアプリケーションや小規模なヒープに適している。GC実行中はすべてのアプリケーションスレッドが停止する(Stop-The-World)。

Parallel GC (Throughput Collector)

複数スレッドでGCを並列実行する。スループット(アプリケーション実行時間の割合)を最大化する。Java 8のデフォルトGC。バッチ処理などスループットが重要な場面に適している。

G1 GC(Garbage-First)

ヒープをリージョンに分割し、ゴミが最も多いリージョンから優先的に回収する。Java 9以降のデフォルトGC。大きなヒープ(数GB〜数十GB)でも比較的短い停止時間を実現する。

ZGC

超低遅延を目指すGC。停止時間を数ミリ秒以内に抑える。数TB規模のヒープに対応。Java 15で正式版となった。コンカレントGCにより、ほとんどの処理をアプリケーションスレッドと並行して実行する。

Shenandoah GC

Red Hatが開発した低遅延GC。ZGCと同様に、GC停止時間をヒープサイズに依存しない一定の短い時間に抑えることを目標としている。

JIT(Just-In-Time)コンパイラ

JITコンパイラは、頻繁に実行されるバイトコード(ホットスポット)をネイティブマシンコードにコンパイルすることで、実行速度を大幅に向上させる。

C1コンパイラ(クライアント)

起動時間を最適化する軽量なコンパイラ。少ない最適化を素早く適用する。

C2コンパイラ(サーバー)

実行時のパフォーマンスを最適化する高度なコンパイラ。インライン展開、ループ最適化、エスケープ解析などの高度な最適化を行う。

階層型コンパイル(Tiered Compilation)

Java 8以降のデフォルト。C1とC2を組み合わせ、起動時にはC1で素早くコンパイルし、ホットスポットが特定された後にC2でより高度な最適化を行う。

JVMパラメータの設定例

メモリ設定

# ヒープサイズの設定
java -Xms512m -Xmx4g -jar application.jar

# Metaspaceの設定
java -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -jar application.jar

# スタックサイズの設定
java -Xss1m -jar application.jar

GC設定

# G1 GCを使用(Java 9+はデフォルト)
java -XX:+UseG1GC \
     -XX:MaxGCPauseMillis=200 \
     -XX:G1HeapRegionSize=16m \
     -jar application.jar

# ZGCを使用
java -XX:+UseZGC \
     -XX:+ZGenerational \
     -Xms4g -Xmx4g \
     -jar application.jar

# Shenandoah GCを使用
java -XX:+UseShenandoahGC \
     -Xms4g -Xmx4g \
     -jar application.jar

GCログ出力

# Java 9+ Unified Logging
java -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=100m \
     -jar application.jar

# GCの詳細ログ
java -Xlog:gc+heap=debug:file=gc-detail.log \
     -jar application.jar

本番環境向け推奨設定

java \
  -server \
  -Xms4g \
  -Xmx4g \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/var/log/java/heapdump.hprof \
  -XX:+UseStringDeduplication \
  -XX:+OptimizeStringConcat \
  -Xlog:gc*:file=/var/log/java/gc.log:time,uptime:filecount=10,filesize=50m \
  -Djava.security.egd=file:/dev/./urandom \
  -jar application.jar

上記の設定では、ヒープを4GBに固定(-Xmsと-Xmxを同値にすることで動的なヒープ拡張を防ぎ、パフォーマンスを安定させる)、G1 GCを使用し、OOMエラー時にヒープダンプを自動出力するよう構成している。

Java言語の基本文法

変数とデータ型

Javaは静的型付け言語であり、すべての変数は宣言時に型を指定する必要がある。データ型はプリミティブ型と参照型の2種類に大別される。

プリミティブ型

Javaには8つのプリミティブ型がある。

サイズ範囲デフォルト値
byte8ビット-128〜1270
short16ビット-32,768〜32,7670
int32ビット-2^31〜2^31-10
long64ビット-2^63〜2^63-10L
float32ビットIEEE 754単精度0.0f
double64ビットIEEE 754倍精度0.0d
char16ビットUnicode文字 ('\u0000'〜'\uffff')'\u0000'
boolean1ビット*true / falsefalse

参照型

参照型はオブジェクトへの参照を保持する。クラス型、インターフェース型、配列型がある。

// プリミティブ型
int count = 42;
long population = 7_900_000_000L;  // 数値リテラルにアンダースコア可(Java 7+)
double pi = 3.14159265358979;
boolean isActive = true;
char grade = 'A';

// 参照型
String name = "Java";
Integer wrappedInt = Integer.valueOf(42);  // ラッパークラス
int[] numbers = {1, 2, 3, 4, 5};          // 配列

// 型推論(Java 10+)
var message = "Hello, Java!";  // String型と推論される
var list = new ArrayList<String>();  // ArrayList<String>型と推論される

オートボクシングとアンボクシング

プリミティブ型とそのラッパークラス間の自動変換(Java 5+)。

// オートボクシング(プリミティブ → ラッパー)
Integer boxed = 100;  // Integer.valueOf(100)と同等

// アンボクシング(ラッパー → プリミティブ)
int unboxed = boxed;  // boxed.intValue()と同等

// 注意: nullのアンボクシングはNullPointerExceptionを引き起こす
Integer nullValue = null;
// int result = nullValue;  // NullPointerException!

制御構文

if-else文

int score = 85;

if (score >= 90) {
    System.out.println("優秀");
} else if (score >= 70) {
    System.out.println("良好");
} else if (score >= 50) {
    System.out.println("合格");
} else {
    System.out.println("不合格");
}

// 三項演算子
String result = (score >= 50) ? "合格" : "不合格";

switch文 / switch式

// 従来のswitch文
int dayOfWeek = 3;
String dayName;
switch (dayOfWeek) {
    case 1:
        dayName = "月曜日";
        break;
    case 2:
        dayName = "火曜日";
        break;
    case 3:
        dayName = "水曜日";
        break;
    default:
        dayName = "不明";
        break;
}

// switch式(Java 14+)
String dayNameModern = switch (dayOfWeek) {
    case 1 -> "月曜日";
    case 2 -> "火曜日";
    case 3 -> "水曜日";
    case 4 -> "木曜日";
    case 5 -> "金曜日";
    case 6 -> "土曜日";
    case 7 -> "日曜日";
    default -> "不明";
};

// switch式でブロックを使用
int numLetters = switch (dayNameModern) {
    case "月曜日", "火曜日", "水曜日", "木曜日", "金曜日" -> {
        System.out.println("平日です");
        yield 3;  // ブロックからの値の返却にはyieldを使用
    }
    case "土曜日", "日曜日" -> {
        System.out.println("週末です");
        yield 3;
    }
    default -> 0;
};

ループ構文

// for文
for (int i = 0; i < 10; i++) {
    System.out.println("カウント: " + i);
}

// 拡張for文(for-each)
String[] fruits = {"りんご", "バナナ", "オレンジ"};
for (String fruit : fruits) {
    System.out.println(fruit);
}

// while文
int count = 0;
while (count < 5) {
    System.out.println("count = " + count);
    count++;
}

// do-while文
int num = 0;
do {
    System.out.println("num = " + num);
    num++;
} while (num < 3);

メソッドとオーバーロード

public class Calculator {
    
    // 基本的なメソッド
    public int add(int a, int b) {
        return a + b;
    }
    
    // メソッドオーバーロード(引数の型が異なる)
    public double add(double a, double b) {
        return a + b;
    }
    
    // メソッドオーバーロード(引数の数が異なる)
    public int add(int a, int b, int c) {
        return a + b + c;
    }
    
    // 可変長引数(varargs)
    public int sum(int... numbers) {
        int total = 0;
        for (int n : numbers) {
            total += n;
        }
        return total;
    }
    
    // staticメソッド
    public static int multiply(int a, int b) {
        return a * b;
    }
}

配列と文字列操作

// 配列の宣言と初期化
int[] numbers = new int[5];
int[] initialized = {10, 20, 30, 40, 50};

// 多次元配列
int[][] matrix = {
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};

// 配列の操作
System.out.println("長さ: " + numbers.length);
Arrays.sort(initialized);
int[] copied = Arrays.copyOf(initialized, 10);  // 配列のコピー(サイズ拡張)
System.out.println(Arrays.toString(initialized));

// 文字列操作
String greeting = "Hello, World!";
System.out.println(greeting.length());           // 13
System.out.println(greeting.substring(0, 5));    // "Hello"
System.out.println(greeting.toUpperCase());      // "HELLO, WORLD!"
System.out.println(greeting.contains("World"));  // true
System.out.println(greeting.replace("World", "Java"));  // "Hello, Java!"
System.out.println(greeting.split(", ").length); // 2

// StringBuilderによる効率的な文字列結合
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append("item").append(i).append(", ");
}
String result = sb.toString();

// Text Blocks(Java 15+)
String json = """
        {
            "name": "Java",
            "version": 21,
            "features": [
                "Virtual Threads",
                "Pattern Matching"
            ]
        }
        """;

// String.format / formatted(Java 15+)
String formatted = "名前: %s, バージョン: %d".formatted("Java", 21);

例外処理

Javaの例外はすべてThrowableクラスから派生する。大きく分けて、Error(回復不能なシステムエラー)、Exception(回復可能な例外)がある。Exceptionはさらに、チェック例外(Checked Exception)と非チェック例外(Unchecked Exception / RuntimeException)に分類される。

// 基本的な例外処理
public class ExceptionExample {
    
    // チェック例外はメソッドシグネチャでthrows宣言が必要
    public String readFile(String path) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
            StringBuilder content = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                content.append(line).append("\n");
            }
            return content.toString();
        }
    }
    
    // try-catch-finally
    public void processData(String data) {
        Connection conn = null;
        try {
            conn = DriverManager.getConnection("jdbc:mysql://localhost/db");
            // データ処理
            if (data == null) {
                throw new IllegalArgumentException("データがnullです");
            }
        } catch (SQLException e) {
            System.err.println("データベースエラー: " + e.getMessage());
        } catch (IllegalArgumentException e) {
            System.err.println("引数エラー: " + e.getMessage());
        } finally {
            // 必ず実行される
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    // try-with-resources(Java 7+)
    // AutoCloseableを実装するリソースは自動的にcloseされる
    public void modernFileRead(String path) {
        try (var reader = new BufferedReader(new FileReader(path));
             var writer = new BufferedWriter(new FileWriter("output.txt"))) {
            
            String line;
            while ((line = reader.readLine()) != null) {
                writer.write(line);
                writer.newLine();
            }
        } catch (FileNotFoundException e) {
            System.err.println("ファイルが見つかりません: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("I/Oエラー: " + e.getMessage());
        }
    }
    
    // マルチキャッチ(Java 7+)
    public void multiCatch() {
        try {
            // 何らかの処理
        } catch (IOException | SQLException e) {
            System.err.println("エラー: " + e.getMessage());
        }
    }
}

// カスタム例外の定義
public class BusinessException extends Exception {
    private final String errorCode;
    
    public BusinessException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
    
    public BusinessException(String errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }
    
    public String getErrorCode() {
        return errorCode;
    }
}

チェック例外 vs 非チェック例外

Throwable
├── Error(非チェック - 回復不能)
│   ├── OutOfMemoryError
│   ├── StackOverflowError
│   └── ...
└── Exception(チェック例外)
    ├── IOException
    ├── SQLException
    ├── ClassNotFoundException
    └── RuntimeException(非チェック例外)
        ├── NullPointerException
        ├── ArrayIndexOutOfBoundsException
        ├── IllegalArgumentException
        ├── ClassCastException
        └── ...

チェック例外はコンパイラが検査し、try-catchまたはメソッドのthrows宣言が必須である。ファイルI/OやDB接続など、外部リソースに依存する処理で使用される。

非チェック例外(RuntimeException)はコンパイラが検査しない。プログラミングエラー(null参照、範囲外アクセスなど)を表し、通常はコードの修正で対処すべきである。

オブジェクト指向プログラミング

クラスとオブジェクト

Javaはオブジェクト指向プログラミング(OOP)を基盤とする言語であり、すべてのコードはクラス内に記述される。クラスはオブジェクトの設計図であり、オブジェクトはクラスから生成されるインスタンスである。

public class Employee {
    // フィールド(インスタンス変数)
    private String name;
    private String department;
    private double salary;
    private static int employeeCount = 0;  // クラス変数(静的変数)
    
    // コンストラクタ
    public Employee(String name, String department, double salary) {
        this.name = name;
        this.department = department;
        this.salary = salary;
        employeeCount++;
    }
    
    // オーバーロードされたコンストラクタ
    public Employee(String name) {
        this(name, "未定", 0.0);
    }
    
    // ゲッター・セッター
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    
    public double getSalary() { return salary; }
    public void setSalary(double salary) {
        if (salary < 0) {
            throw new IllegalArgumentException("給与は0以上でなければなりません");
        }
        this.salary = salary;
    }
    
    // インスタンスメソッド
    public double calculateAnnualSalary() {
        return salary * 12;
    }
    
    // 静的メソッド
    public static int getEmployeeCount() {
        return employeeCount;
    }
    
    @Override
    public String toString() {
        return "Employee{name='%s', department='%s', salary=%.2f}"
            .formatted(name, department, salary);
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return Objects.equals(name, employee.name) 
            && Objects.equals(department, employee.department);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, department);
    }
}

継承(Inheritance)

継承により、既存クラスの機能を受け継いだ新しいクラスを作成できる。Javaは単一継承のみをサポートする。

// 基底クラス(親クラス)
public class Shape {
    protected String color;
    
    public Shape(String color) {
        this.color = color;
    }
    
    public double area() {
        return 0.0;
    }
    
    public String getColor() {
        return color;
    }
}

// 派生クラス(子クラス)
public class Circle extends Shape {
    private double radius;
    
    public Circle(String color, double radius) {
        super(color);  // 親クラスのコンストラクタ呼び出し
        this.radius = radius;
    }
    
    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
    
    @Override
    public String toString() {
        return "Circle{color='%s', radius=%.2f, area=%.2f}"
            .formatted(color, radius, area());
    }
}

public class Rectangle extends Shape {
    private double width;
    private double height;
    
    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }
    
    @Override
    public double area() {
        return width * height;
    }
}

ポリモーフィズム(Polymorphism)

ポリモーフィズムにより、同一のインターフェースで異なる実装を呼び出すことができる。

public class ShapeDemo {
    public static void main(String[] args) {
        // ポリモーフィズム: 親クラス型の変数で子クラスのインスタンスを参照
        Shape[] shapes = {
            new Circle("赤", 5.0),
            new Rectangle("青", 4.0, 6.0),
            new Circle("緑", 3.0)
        };
        
        // 同じメソッド呼び出しで異なる動作
        for (Shape shape : shapes) {
            System.out.printf("色: %s, 面積: %.2f%n", 
                shape.getColor(), shape.area());
        }
    }
}

カプセル化(Encapsulation)

カプセル化は、データ(フィールド)とそれを操作するメソッドをひとつにまとめ、外部からの不正なアクセスを防ぐ仕組みである。

アクセス修飾子は以下の4種類がある。

修飾子クラス内パッケージ内サブクラスすべて
private×××
デフォルト(修飾子なし)××
protected×
public
public class BankAccount {
    // カプセル化: フィールドはprivate
    private String accountNumber;
    private double balance;
    private final List<String> transactionHistory;
    
    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
        this.transactionHistory = new ArrayList<>();
        transactionHistory.add("口座開設: %.2f".formatted(initialBalance));
    }
    
    // 公開インターフェースでデータを保護
    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("入金額は正の値でなければなりません");
        }
        balance += amount;
        transactionHistory.add("入金: +%.2f".formatted(amount));
    }
    
    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("出金額は正の値でなければなりません");
        }
        if (amount > balance) {
            throw new IllegalStateException("残高不足です");
        }
        balance -= amount;
        transactionHistory.add("出金: -%.2f".formatted(amount));
    }
    
    public double getBalance() {
        return balance;
    }
    
    // 不変リストとして返却(防御的コピー)
    public List<String> getTransactionHistory() {
        return Collections.unmodifiableList(transactionHistory);
    }
}

インターフェースと抽象クラス

// インターフェース
public interface Payable {
    double calculatePay();
    
    // デフォルトメソッド(Java 8+)
    default String getPaymentMethod() {
        return "銀行振込";
    }
    
    // 静的メソッド(Java 8+)
    static Payable createFixed(double amount) {
        return () -> amount;
    }
    
    // privateメソッド(Java 9+)
    private double applyTax(double amount) {
        return amount * 0.8;
    }
}

// 抽象クラス
public abstract class AbstractEmployee implements Payable {
    protected String name;
    protected String id;
    
    public AbstractEmployee(String name, String id) {
        this.name = name;
        this.id = id;
    }
    
    // 抽象メソッド
    public abstract String getEmployeeType();
    
    // 具象メソッド
    public String getInfo() {
        return "%s [%s]: %s".formatted(name, id, getEmployeeType());
    }
}

// 具象クラス
public class FullTimeEmployee extends AbstractEmployee {
    private double monthlySalary;
    
    public FullTimeEmployee(String name, String id, double monthlySalary) {
        super(name, id);
        this.monthlySalary = monthlySalary;
    }
    
    @Override
    public double calculatePay() {
        return monthlySalary;
    }
    
    @Override
    public String getEmployeeType() {
        return "正社員";
    }
}

public class ContractEmployee extends AbstractEmployee {
    private double hourlyRate;
    private int hoursWorked;
    
    public ContractEmployee(String name, String id, 
                           double hourlyRate, int hoursWorked) {
        super(name, id);
        this.hourlyRate = hourlyRate;
        this.hoursWorked = hoursWorked;
    }
    
    @Override
    public double calculatePay() {
        return hourlyRate * hoursWorked;
    }
    
    @Override
    public String getEmployeeType() {
        return "契約社員";
    }
}

デザインパターン

Singletonパターン

public class DatabaseConnection {
    // volatile: 他スレッドからの変更を即座に反映
    private static volatile DatabaseConnection instance;
    private final Connection connection;
    
    private DatabaseConnection() {
        // プライベートコンストラクタ
        try {
            this.connection = DriverManager.getConnection(
                "jdbc:mysql://localhost/mydb", "user", "password");
        } catch (SQLException e) {
            throw new RuntimeException("DB接続に失敗しました", e);
        }
    }
    
    // ダブルチェックロッキング
    public static DatabaseConnection getInstance() {
        if (instance == null) {
            synchronized (DatabaseConnection.class) {
                if (instance == null) {
                    instance = new DatabaseConnection();
                }
            }
        }
        return instance;
    }
}

// より推奨されるEnum Singletonパターン
public enum AppConfig {
    INSTANCE;
    
    private final Properties properties = new Properties();
    
    AppConfig() {
        try (var is = getClass().getResourceAsStream("/config.properties")) {
            properties.load(is);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    public String get(String key) {
        return properties.getProperty(key);
    }
}

Factoryパターン

public interface Notification {
    void send(String message);
}

public class EmailNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("メール送信: " + message);
    }
}

public class SmsNotification implements Notification {
    @Override
    public void send(String message) {
        System.out.println("SMS送信: " + message);
    }
}

public class NotificationFactory {
    public static Notification create(String type) {
        return switch (type.toLowerCase()) {
            case "email" -> new EmailNotification();
            case "sms"   -> new SmsNotification();
            default -> throw new IllegalArgumentException("不明な通知タイプ: " + type);
        };
    }
}

Builderパターン

public class HttpRequest {
    private final String url;
    private final String method;
    private final Map<String, String> headers;
    private final String body;
    private final int timeout;
    
    private HttpRequest(Builder builder) {
        this.url = builder.url;
        this.method = builder.method;
        this.headers = Map.copyOf(builder.headers);
        this.body = builder.body;
        this.timeout = builder.timeout;
    }
    
    public static class Builder {
        private final String url;            // 必須
        private String method = "GET";       // デフォルト値
        private Map<String, String> headers = new HashMap<>();
        private String body;
        private int timeout = 30000;
        
        public Builder(String url) {
            this.url = Objects.requireNonNull(url);
        }
        
        public Builder method(String method) { this.method = method; return this; }
        public Builder header(String key, String value) { headers.put(key, value); return this; }
        public Builder body(String body) { this.body = body; return this; }
        public Builder timeout(int timeout) { this.timeout = timeout; return this; }
        
        public HttpRequest build() {
            return new HttpRequest(this);
        }
    }
}

// 使用例
HttpRequest request = new HttpRequest.Builder("https://api.example.com/users")
    .method("POST")
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer token123")
    .body("{\"name\": \"太郎\"}")
    .timeout(5000)
    .build();

SOLIDの原則

OOPの設計原則として「SOLID」がある。

  1. S(Single Responsibility Principle - 単一責任の原則): クラスは一つの責任だけを持つべき
  2. O(Open/Closed Principle - 開放閉鎖の原則): 拡張に対して開き、修正に対して閉じるべき
  3. L(Liskov Substitution Principle - リスコフの置換原則): サブクラスは親クラスの代わりに使えるべき
  4. I(Interface Segregation Principle - インターフェース分離の原則): クライアントが使わないメソッドへの依存を強制しない
  5. D(Dependency Inversion Principle - 依存性逆転の原則): 上位モジュールは下位モジュールに依存すべきでなく、両方が抽象に依存すべき
// インターフェース分離の原則(ISP)の例
// 悪い例: 一つの巨大なインターフェース
// public interface Worker { void work(); void eat(); void sleep(); }

// 良い例: 役割ごとにインターフェースを分離
public interface Workable { void work(); }
public interface Feedable { void eat(); }
public interface Sleepable { void sleep(); }

// 人間は全機能を持つ
public class Human implements Workable, Feedable, Sleepable {
    @Override public void work() { /* ... */ }
    @Override public void eat()  { /* ... */ }
    @Override public void sleep() { /* ... */ }
}

// ロボットは仕事だけ
public class Robot implements Workable {
    @Override public void work() { /* ... */ }
}

ジェネリクスとコレクションフレームワーク

ジェネリクスの仕組み

ジェネリクス(Generics)は、Java 5で導入された型パラメータ化の仕組みである。コンパイル時に型安全性を保証し、キャストの必要性を排除する。

基本的なジェネリクス

// ジェネリッククラス
public class Box<T> {
    private T content;
    
    public Box(T content) {
        this.content = content;
    }
    
    public T getContent() {
        return content;
    }
    
    public void setContent(T content) {
        this.content = content;
    }
}

// 使用例
Box<String> stringBox = new Box<>("Hello");
String value = stringBox.getContent();  // キャスト不要

Box<Integer> intBox = new Box<>(42);
int number = intBox.getContent();  // オートアンボクシング

// 複数の型パラメータ
public class Pair<K, V> {
    private K key;
    private V value;
    
    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }
    
    public K getKey() { return key; }
    public V getValue() { return value; }
    
    @Override
    public String toString() {
        return "(%s, %s)".formatted(key, value);
    }
}

Pair<String, Integer> pair = new Pair<>("年齢", 30);

境界付き型パラメータ

// 上限境界(extends): TはNumber以下の型でなければならない
public class MathUtils {
    public static <T extends Number & Comparable<T>> T max(T a, T b) {
        return (a.compareTo(b) >= 0) ? a : b;
    }
}

// 使用例
int maxInt = MathUtils.max(10, 20);       // 20
double maxDouble = MathUtils.max(3.14, 2.71);  // 3.14

ワイルドカード

public class WildcardExample {
    
    // 非境界ワイルドカード: 任意の型
    public static void printList(List<?> list) {
        for (Object item : list) {
            System.out.println(item);
        }
    }
    
    // 上限境界ワイルドカード(共変): Numberのサブクラス
    // Producer Extends(PECS原則: 読み取り用)
    public static double sum(List<? extends Number> numbers) {
        double total = 0;
        for (Number num : numbers) {
            total += num.doubleValue();
        }
        return total;
    }
    
    // 下限境界ワイルドカード(反変): Integerのスーパークラス
    // Consumer Super(PECS原則: 書き込み用)
    public static void addIntegers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }
}

// PECS: Producer Extends, Consumer Super
// リストから読み取る場合は <? extends T>
// リストに書き込む場合は <? super T>

型消去(Type Erasure)

Javaのジェネリクスはコンパイル時にのみ存在し、実行時には型パラメータが消去される。これを型消去と呼ぶ。

// コンパイル前
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();

// 型消去後(実行時)
// List strings = new ArrayList();
// List integers = new ArrayList();

// そのため、以下は不可能
// if (list instanceof List<String>) {}  // コンパイルエラー
// new T();  // コンパイルエラー
// new T[10];  // コンパイルエラー

ジェネリックメソッド

public class GenericMethods {
    
    // ジェネリックメソッド
    public static <T> List<T> listOf(T... elements) {
        return Arrays.asList(elements);
    }
    
    // 型パラメータの推論
    public static <T extends Comparable<T>> T findMax(List<T> list) {
        if (list.isEmpty()) {
            throw new IllegalArgumentException("リストが空です");
        }
        T max = list.get(0);
        for (T item : list) {
            if (item.compareTo(max) > 0) {
                max = item;
            }
        }
        return max;
    }
}

コレクションフレームワーク

Java Collections Frameworkは、データの格納と操作のための統一されたアーキテクチャを提供する。

コレクション階層

Iterable<T>
└── Collection<T>
    ├── List<T>(順序あり、重複可)
    │   ├── ArrayList<T>
    │   ├── LinkedList<T>
    │   └── Vector<T>(同期化、レガシー)
    ├── Set<T>(重複不可)
    │   ├── HashSet<T>
    │   ├── LinkedHashSet<T>(挿入順)
    │   └── TreeSet<T>(ソート順)
    └── Queue<T>
        ├── LinkedList<T>
        ├── PriorityQueue<T>
        └── ArrayDeque<T>

Map<K, V>(キー重複不可)
├── HashMap<K, V>
├── LinkedHashMap<K, V>(挿入順)
├── TreeMap<K, V>(キーソート順)
├── Hashtable<K, V>(同期化、レガシー)
└── ConcurrentHashMap<K, V>(並行処理対応)

List

// ArrayList: 内部配列ベース、ランダムアクセスO(1)、挿入・削除O(n)
List<String> arrayList = new ArrayList<>();
arrayList.add("Java");
arrayList.add("Python");
arrayList.add("Go");
arrayList.add(1, "Kotlin");  // インデックス1に挿入
System.out.println(arrayList.get(0));  // "Java"
arrayList.remove("Go");
arrayList.set(0, "Java 21");

// LinkedList: 双方向リンクリスト、挿入・削除O(1)、ランダムアクセスO(n)
List<String> linkedList = new LinkedList<>();
linkedList.add("First");
linkedList.addFirst("Zero");    // Dequeインターフェース
linkedList.addLast("Last");

// 不変リスト(Java 9+)
List<String> immutableList = List.of("A", "B", "C");
// immutableList.add("D");  // UnsupportedOperationException

// 可変コピーを作成
List<String> mutableCopy = new ArrayList<>(immutableList);
mutableCopy.add("D");  // OK

Set

// HashSet: ハッシュテーブルベース、O(1)の検索・追加・削除
Set<String> hashSet = new HashSet<>();
hashSet.add("Apple");
hashSet.add("Banana");
hashSet.add("Apple");  // 重複は無視される
System.out.println(hashSet.size());  // 2

// LinkedHashSet: 挿入順序を保持
Set<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("C");
linkedHashSet.add("A");
linkedHashSet.add("B");
// イテレーション順: C, A, B

// TreeSet: ソート順(自然順序またはComparator)
Set<String> treeSet = new TreeSet<>();
treeSet.add("Banana");
treeSet.add("Apple");
treeSet.add("Cherry");
// イテレーション順: Apple, Banana, Cherry

// 不変Set(Java 9+)
Set<Integer> immutableSet = Set.of(1, 2, 3, 4, 5);

// 集合演算
Set<Integer> setA = new HashSet<>(Set.of(1, 2, 3, 4, 5));
Set<Integer> setB = Set.of(3, 4, 5, 6, 7);

// 和集合
Set<Integer> union = new HashSet<>(setA);
union.addAll(setB);  // {1, 2, 3, 4, 5, 6, 7}

// 積集合
Set<Integer> intersection = new HashSet<>(setA);
intersection.retainAll(setB);  // {3, 4, 5}

// 差集合
Set<Integer> difference = new HashSet<>(setA);
difference.removeAll(setB);  // {1, 2}

Map

// HashMap
Map<String, Integer> scores = new HashMap<>();
scores.put("田中", 85);
scores.put("佐藤", 92);
scores.put("鈴木", 78);

int tanaka = scores.get("田中");  // 85
int unknown = scores.getOrDefault("山田", 0);  // 0

// イテレーション
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    System.out.printf("%s: %d%n", entry.getKey(), entry.getValue());
}

// Java 8+ forEach
scores.forEach((name, score) -> 
    System.out.printf("%s: %d%n", name, score));

// compute系メソッド
scores.computeIfAbsent("山田", k -> 70);  // 存在しない場合のみ追加
scores.computeIfPresent("田中", (k, v) -> v + 10);  // 95
scores.merge("佐藤", 5, Integer::sum);  // 92 + 5 = 97

// TreeMap: キーの自然順序でソート
Map<String, Integer> sortedMap = new TreeMap<>(scores);

// 不変Map(Java 9+)
Map<String, Integer> immutableMap = Map.of(
    "one", 1,
    "two", 2,
    "three", 3
);

// 10個超の場合
Map<String, Integer> largeMap = Map.ofEntries(
    Map.entry("one", 1),
    Map.entry("two", 2),
    Map.entry("three", 3)
    // ...
);

Queue / Deque

// PriorityQueue: 優先度付きキュー(最小ヒープ)
Queue<Integer> pq = new PriorityQueue<>();
pq.offer(30);
pq.offer(10);
pq.offer(20);
System.out.println(pq.poll());  // 10(最小値)

// カスタムComparatorで最大ヒープ
Queue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());

// ArrayDeque: 両端キュー(スタックとしてもキューとしても使用可能)
Deque<String> deque = new ArrayDeque<>();
deque.push("A");  // スタックとして: 先頭に追加
deque.push("B");
deque.push("C");
System.out.println(deque.pop());  // "C"(LIFO)

deque.offer("X");  // キューとして: 末尾に追加
System.out.println(deque.poll());  // 先頭から取得

ソートとComparable / Comparator

// Comparable: 自然順序の定義
public class Student implements Comparable<Student> {
    private String name;
    private int score;
    
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
    
    @Override
    public int compareTo(Student other) {
        return Integer.compare(this.score, other.score);
    }
    
    // getter, toString省略
}

List<Student> students = new ArrayList<>(List.of(
    new Student("田中", 85),
    new Student("佐藤", 92),
    new Student("鈴木", 78)
));

// Comparable(自然順序)でソート
Collections.sort(students);

// Comparatorでカスタムソート
students.sort(Comparator.comparing(Student::getName));  // 名前順
students.sort(Comparator.comparingInt(Student::getScore).reversed());  // 成績降順

// 複合ソート
students.sort(
    Comparator.comparingInt(Student::getScore)
              .reversed()
              .thenComparing(Student::getName)
);

パフォーマンス比較

操作ArrayListLinkedListHashSetTreeSetHashMapTreeMap
追加(末尾)O(1)*O(1)O(1)*O(log n)O(1)*O(log n)
追加(先頭)O(n)O(1)----
検索(インデックス)O(1)O(n)----
検索(値)O(n)O(n)O(1)*O(log n)O(1)*O(log n)
削除O(n)O(1)**O(1)*O(log n)O(1)*O(log n)

* 平均計算量。ハッシュ衝突がない場合。
** ノードが特定済みの場合。検索を含むとO(n)。

Stream APIと関数型プログラミング

ラムダ式と関数型インターフェース

Java 8で導入されたラムダ式は、関数型プログラミングのパラダイムをJavaに持ち込んだ。ラムダ式は、関数型インターフェース(単一の抽象メソッドを持つインターフェース)の簡潔な実装を提供する。

ラムダ式の基本構文

// ラムダ式の構文: (引数) -> { 本体 }

// 従来の匿名クラス
Runnable oldStyle = new Runnable() {
    @Override
    public void run() {
        System.out.println("従来の匿名クラス");
    }
};

// ラムダ式
Runnable lambdaStyle = () -> System.out.println("ラムダ式");

// 引数ありのラムダ式
Comparator<String> comparator = (a, b) -> a.length() - b.length();

// 複数行のラムダ式
Comparator<String> detailedComparator = (a, b) -> {
    int lengthDiff = a.length() - b.length();
    if (lengthDiff != 0) return lengthDiff;
    return a.compareTo(b);
};

標準関数型インターフェース(java.util.function)

import java.util.function.*;

// Function<T, R>: T → R の変換
Function<String, Integer> length = String::length;
Function<Integer, String> intToStr = i -> "値: " + i;
// 関数合成
Function<String, String> lengthAsString = length.andThen(intToStr);
System.out.println(lengthAsString.apply("Hello"));  // "値: 5"

// Predicate<T>: T → boolean の判定
Predicate<String> isNotEmpty = s -> !s.isEmpty();
Predicate<String> startsWithJ = s -> s.startsWith("J");
Predicate<String> combined = isNotEmpty.and(startsWithJ);
System.out.println(combined.test("Java"));  // true

// Consumer<T>: T → void の処理
Consumer<String> printer = System.out::println;
Consumer<String> upperPrinter = s -> System.out.println(s.toUpperCase());
printer.andThen(upperPrinter).accept("hello");
// "hello"
// "HELLO"

// Supplier<T>: () → T の生成
Supplier<LocalDateTime> now = LocalDateTime::now;
Supplier<List<String>> listFactory = ArrayList::new;

// BiFunction<T, U, R>: (T, U) → R
BiFunction<String, String, String> concat = String::concat;

// UnaryOperator<T>: T → T (Function<T, T>の特殊形)
UnaryOperator<String> toUpper = String::toUpperCase;

// BinaryOperator<T>: (T, T) → T (BiFunction<T, T, T>の特殊形)
BinaryOperator<Integer> add = Integer::sum;

Stream APIの操作

Stream APIは、コレクションのデータ処理をパイプライン形式で記述するための仕組みである。

Streamの生成

// コレクションから
List<String> list = List.of("Java", "Python", "Go", "Rust", "Kotlin");
Stream<String> stream = list.stream();

// 配列から
String[] array = {"A", "B", "C"};
Stream<String> arrayStream = Arrays.stream(array);

// 値の直接指定
Stream<String> ofStream = Stream.of("X", "Y", "Z");

// 無限ストリーム
Stream<Integer> infinite = Stream.iterate(0, n -> n + 2);  // 0, 2, 4, 6...
Stream<Double> randoms = Stream.generate(Math::random);

// 範囲指定
IntStream range = IntStream.range(1, 10);       // 1〜9
IntStream rangeClosed = IntStream.rangeClosed(1, 10);  // 1〜10

// 文字列から
IntStream chars = "Hello".chars();  // 各文字のint値

// ファイルから
// Stream<String> lines = Files.lines(Path.of("data.txt"));

中間操作(Intermediate Operations)

中間操作は遅延評価される。終端操作が呼ばれるまで実行されない。

List<String> languages = List.of(
    "Java", "Python", "JavaScript", "Go", "Rust", "Kotlin", "Java", "C++"
);

// filter: 条件に合う要素のみ残す
List<String> jLanguages = languages.stream()
    .filter(s -> s.startsWith("J"))
    .toList();  // [Java, JavaScript, Java]

// map: 各要素を変換
List<Integer> lengths = languages.stream()
    .map(String::length)
    .toList();  // [4, 6, 10, 2, 4, 6, 4, 3]

// flatMap: 各要素をStreamに変換して平坦化
List<List<Integer>> nested = List.of(
    List.of(1, 2, 3),
    List.of(4, 5),
    List.of(6, 7, 8, 9)
);
List<Integer> flat = nested.stream()
    .flatMap(Collection::stream)
    .toList();  // [1, 2, 3, 4, 5, 6, 7, 8, 9]

// distinct: 重複除去
List<String> unique = languages.stream()
    .distinct()
    .toList();

// sorted: ソート
List<String> sorted = languages.stream()
    .sorted()
    .toList();

List<String> sortedByLength = languages.stream()
    .sorted(Comparator.comparingInt(String::length))
    .toList();

// peek: デバッグ用(副作用を伴う中間操作)
List<String> result = languages.stream()
    .filter(s -> s.length() > 3)
    .peek(s -> System.out.println("フィルタ通過: " + s))
    .map(String::toUpperCase)
    .peek(s -> System.out.println("変換後: " + s))
    .toList();

// limit / skip
List<String> firstThree = languages.stream().limit(3).toList();
List<String> afterTwo = languages.stream().skip(2).toList();

// takeWhile / dropWhile(Java 9+)
List<Integer> numbers = List.of(2, 4, 6, 7, 8, 10);
List<Integer> evenPrefix = numbers.stream()
    .takeWhile(n -> n % 2 == 0)
    .toList();  // [2, 4, 6](7で停止)

終端操作(Terminal Operations)

List<Employee> employees = List.of(
    new Employee("田中", "開発", 500000),
    new Employee("佐藤", "営業", 450000),
    new Employee("鈴木", "開発", 550000),
    new Employee("高橋", "人事", 480000),
    new Employee("山田", "開発", 520000)
);

// forEach
employees.stream().forEach(System.out::println);

// collect: コレクションへの変換
List<String> names = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.toList());
// Java 16+ では .toList() が使用可能

// reduce: 集約
double totalSalary = employees.stream()
    .mapToDouble(Employee::getSalary)
    .reduce(0.0, Double::sum);

// count, min, max
long count = employees.stream().count();
Optional<Employee> highest = employees.stream()
    .max(Comparator.comparingDouble(Employee::getSalary));

// anyMatch, allMatch, noneMatch
boolean hasDev = employees.stream()
    .anyMatch(e -> e.getDepartment().equals("開発"));
boolean allHighSalary = employees.stream()
    .allMatch(e -> e.getSalary() > 400000);

// findFirst, findAny
Optional<Employee> firstDev = employees.stream()
    .filter(e -> e.getDepartment().equals("開発"))
    .findFirst();

// toArray
String[] nameArray = employees.stream()
    .map(Employee::getName)
    .toArray(String[]::new);

Collectorsの活用

// グループ化
Map<String, List<Employee>> byDept = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment));

// グループ化 + 集約
Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)
    ));

// グループ化 + カウント
Map<String, Long> countByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.counting()
    ));

// パーティショニング(2分割)
Map<Boolean, List<Employee>> partitioned = employees.stream()
    .collect(Collectors.partitioningBy(
        e -> e.getSalary() > 500000
    ));

// 文字列結合
String nameList = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.joining(", ", "[", "]"));
// "[田中, 佐藤, 鈴木, 高橋, 山田]"

// 統計情報
DoubleSummaryStatistics stats = employees.stream()
    .collect(Collectors.summarizingDouble(Employee::getSalary));
System.out.println("平均: " + stats.getAverage());
System.out.println("最大: " + stats.getMax());
System.out.println("最小: " + stats.getMin());
System.out.println("合計: " + stats.getSum());
System.out.println("件数: " + stats.getCount());

// Mapへの変換
Map<String, Double> salaryMap = employees.stream()
    .collect(Collectors.toMap(
        Employee::getName,
        Employee::getSalary
    ));

// Mapへの変換(キー重複時のマージ関数指定)
Map<String, Double> deptTotalSalary = employees.stream()
    .collect(Collectors.toMap(
        Employee::getDepartment,
        Employee::getSalary,
        Double::sum  // 重複キーの場合は合計
    ));

Optionalクラス

Optional<T>は、値が存在するかしないかを表すコンテナクラスである。NullPointerExceptionを防ぐために使用される。

// Optionalの生成
Optional<String> present = Optional.of("Hello");
Optional<String> empty = Optional.empty();
Optional<String> nullable = Optional.ofNullable(null);  // empty

// 値の取得
present.get();                        // "Hello"(空の場合はNoSuchElementException)
present.orElse("Default");            // "Hello"
empty.orElse("Default");              // "Default"
empty.orElseGet(() -> "Computed");     // 遅延評価
empty.orElseThrow();                  // NoSuchElementException
empty.orElseThrow(() -> new BusinessException("E001", "値がありません"));

// 存在チェック
present.isPresent();  // true
present.isEmpty();    // false(Java 11+)

// 関数型操作
Optional<String> upper = present.map(String::toUpperCase);  // Optional["HELLO"]
Optional<Integer> length = present.map(String::length);      // Optional[5]

// flatMap: Optional<Optional<T>>の平坦化
Optional<String> flatMapped = present
    .flatMap(s -> Optional.of(s + " World"));

// filter
Optional<String> filtered = present
    .filter(s -> s.length() > 3);  // Optional["Hello"]

// ifPresent / ifPresentOrElse
present.ifPresent(System.out::println);
empty.ifPresentOrElse(
    System.out::println,
    () -> System.out.println("値がありません")
);

// or(Java 9+)
Optional<String> result = empty
    .or(() -> Optional.of("代替値"));  // Optional["代替値"]

// stream(Java 9+)
List<String> values = Stream.of(
    Optional.of("A"),
    Optional.empty(),
    Optional.of("B"),
    Optional.empty(),
    Optional.of("C")
)
.flatMap(Optional::stream)
.toList();  // ["A", "B", "C"]

メソッド参照

// 静的メソッド参照: ClassName::staticMethod
Function<String, Integer> parseInt = Integer::parseInt;

// インスタンスメソッド参照(特定インスタンス): instance::method
String prefix = "Java";
Predicate<String> startsWith = prefix::startsWith;

// インスタンスメソッド参照(任意インスタンス): ClassName::method
Function<String, String> toUpper = String::toUpperCase;

// コンストラクタ参照: ClassName::new
Supplier<ArrayList<String>> listFactory = ArrayList::new;
Function<String, StringBuilder> sbFactory = StringBuilder::new;

並列ストリーム

// 並列ストリームの生成
List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000)
    .boxed()
    .toList();

// 並列処理
long sum = numbers.parallelStream()
    .filter(n -> n % 2 == 0)
    .mapToLong(Integer::longValue)
    .sum();

// 注意: 並列ストリームは常に速いわけではない
// - 小さなデータセットではオーバーヘッドが大きい
// - 順序依存の処理には不向き
// - 共有可変状態を避ける必要がある
// - ForkJoinPool.commonPool()を使用するため、スレッド数に注意

// カスタムForkJoinPoolで実行
ForkJoinPool customPool = new ForkJoinPool(4);
try {
    long result = customPool.submit(() ->
        numbers.parallelStream()
            .filter(n -> n % 3 == 0)
            .mapToLong(Integer::longValue)
            .sum()
    ).get();
} catch (Exception e) {
    e.printStackTrace();
} finally {
    customPool.shutdown();
}

並行処理とマルチスレッド

Threadクラスと Runnableインターフェース

Javaは言語レベルでマルチスレッドプログラミングをサポートしている。スレッドの作成方法は主に2つある。

// 方法1: Threadクラスを継承
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.printf("[%s] カウント: %d%n", 
                Thread.currentThread().getName(), i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }
}

// 方法2: Runnableインターフェースを実装(推奨)
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnableで実行: " + Thread.currentThread().getName());
    }
}

// 方法3: ラムダ式(推奨)
public class ThreadExample {
    public static void main(String[] args) {
        Thread t1 = new MyThread();
        t1.setName("Thread-A");
        t1.start();
        
        Thread t2 = new Thread(new MyRunnable(), "Thread-B");
        t2.start();
        
        Thread t3 = new Thread(() -> {
            System.out.println("ラムダスレッド: " + Thread.currentThread().getName());
        }, "Thread-C");
        t3.start();
        
        // Callable: 戻り値を持つタスク
        Callable<Integer> callable = () -> {
            Thread.sleep(1000);
            return 42;
        };
    }
}

synchronized, volatile, Lock

synchronized

public class Counter {
    private int count = 0;
    
    // synchronizedメソッド
    public synchronized void increment() {
        count++;
    }
    
    // synchronizedブロック
    public void incrementWithBlock() {
        synchronized (this) {
            count++;
        }
    }
    
    public synchronized int getCount() {
        return count;
    }
}

// synchronizedを使ったスレッドセーフなシングルトン
public class ThreadSafeSingleton {
    private static volatile ThreadSafeSingleton instance;
    
    private ThreadSafeSingleton() {}
    
    public static ThreadSafeSingleton getInstance() {
        if (instance == null) {
            synchronized (ThreadSafeSingleton.class) {
                if (instance == null) {
                    instance = new ThreadSafeSingleton();
                }
            }
        }
        return instance;
    }
}

volatile

volatileキーワードは、変数の読み書きがメインメモリに対して直接行われることを保証する。

public class VolatileExample {
    // volatileなしでは、スレッドが独自のキャッシュから読み取る可能性がある
    private volatile boolean running = true;
    
    public void start() {
        new Thread(() -> {
            while (running) {
                // 処理を実行
            }
            System.out.println("スレッド停止");
        }).start();
    }
    
    public void stop() {
        running = false;  // 他のスレッドから即座に見える
    }
}

Lock(ReentrantLock)

java.util.concurrent.locksパッケージは、synchronizedよりも柔軟なロック機構を提供する。

import java.util.concurrent.locks.*;

public class BankAccountWithLock {
    private double balance;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition sufficientFunds = lock.newCondition();
    
    public BankAccountWithLock(double initialBalance) {
        this.balance = initialBalance;
    }
    
    public void deposit(double amount) {
        lock.lock();
        try {
            balance += amount;
            System.out.printf("入金: %.2f, 残高: %.2f%n", amount, balance);
            sufficientFunds.signalAll();  // 待機中のスレッドに通知
        } finally {
            lock.unlock();  // 必ずfinallyでアンロック
        }
    }
    
    public void withdraw(double amount) throws InterruptedException {
        lock.lock();
        try {
            while (balance < amount) {
                System.out.printf("残高不足。%.2f が必要、現在: %.2f%n", amount, balance);
                sufficientFunds.await();  // 条件が満たされるまで待機
            }
            balance -= amount;
            System.out.printf("出金: %.2f, 残高: %.2f%n", amount, balance);
        } finally {
            lock.unlock();
        }
    }
    
    // tryLockによるタイムアウト付きロック取得
    public boolean tryWithdraw(double amount, long timeoutMs) {
        try {
            if (lock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
                try {
                    if (balance >= amount) {
                        balance -= amount;
                        return true;
                    }
                    return false;
                } finally {
                    lock.unlock();
                }
            }
            return false;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }
}

// ReadWriteLock: 読み取りは並行、書き込みは排他
public class CachedData<T> {
    private T data;
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    
    public T read() {
        rwLock.readLock().lock();
        try {
            return data;
        } finally {
            rwLock.readLock().unlock();
        }
    }
    
    public void write(T newData) {
        rwLock.writeLock().lock();
        try {
            data = newData;
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

ExecutorServiceとスレッドプール

直接Threadを作成する代わりに、ExecutorServiceを使うことでスレッドの再利用と管理が効率化される。

import java.util.concurrent.*;

public class ExecutorExample {
    
    public static void main(String[] args) throws Exception {
        // 固定サイズのスレッドプール
        ExecutorService fixedPool = Executors.newFixedThreadPool(4);
        
        // キャッシュスレッドプール(必要に応じてスレッドを生成・再利用)
        ExecutorService cachedPool = Executors.newCachedThreadPool();
        
        // 単一スレッドプール(タスクを順番に実行)
        ExecutorService singlePool = Executors.newSingleThreadExecutor();
        
        // スケジュール実行
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
        
        // カスタムスレッドプール(推奨)
        ThreadPoolExecutor customPool = new ThreadPoolExecutor(
            4,                        // コアスレッド数
            8,                        // 最大スレッド数
            60L, TimeUnit.SECONDS,    // アイドルスレッドの生存時間
            new LinkedBlockingQueue<>(100),  // タスクキュー
            new ThreadPoolExecutor.CallerRunsPolicy()  // 拒否ポリシー
        );
        
        // Runnableの送信
        fixedPool.execute(() -> System.out.println("非同期タスク"));
        
        // Callableの送信(戻り値あり)
        Future<String> future = fixedPool.submit(() -> {
            Thread.sleep(1000);
            return "結果";
        });
        String result = future.get(5, TimeUnit.SECONDS);  // タイムアウト付き
        
        // 複数タスクの一括実行
        List<Callable<Integer>> tasks = List.of(
            () -> { Thread.sleep(1000); return 1; },
            () -> { Thread.sleep(2000); return 2; },
            () -> { Thread.sleep(500);  return 3; }
        );
        
        // すべて完了を待つ
        List<Future<Integer>> futures = fixedPool.invokeAll(tasks);
        for (Future<Integer> f : futures) {
            System.out.println("結果: " + f.get());
        }
        
        // 最初に完了したタスクの結果を取得
        Integer firstResult = fixedPool.invokeAny(tasks);
        
        // スケジュール実行
        scheduler.schedule(() -> System.out.println("3秒後に実行"), 3, TimeUnit.SECONDS);
        scheduler.scheduleAtFixedRate(
            () -> System.out.println("1秒間隔で実行"),
            0, 1, TimeUnit.SECONDS
        );
        
        // シャットダウン
        fixedPool.shutdown();  // 新しいタスクを受け付けず、既存タスクの完了を待つ
        if (!fixedPool.awaitTermination(60, TimeUnit.SECONDS)) {
            fixedPool.shutdownNow();  // 強制終了
        }
    }
}

CompletableFuture

CompletableFutureは、非同期処理を関数型スタイルで組み立てるためのクラスである(Java 8+)。

import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {
    
    // 非同期処理の基本
    public static void basicExample() {
        // 非同期タスクの実行
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // 別スレッドで実行
            simulateDelay(1000);
            return "Hello";
        });
        
        // 結果の変換
        CompletableFuture<String> transformed = future
            .thenApply(s -> s + " World")           // 同期変換
            .thenApply(String::toUpperCase);         // "HELLO WORLD"
        
        // 非同期チェーン
        CompletableFuture<String> chained = future
            .thenCompose(s -> CompletableFuture.supplyAsync(() -> s + " Async"));
        
        // 副作用
        future.thenAccept(System.out::println);  // 結果を消費
        future.thenRun(() -> System.out.println("完了"));  // 結果を使わない
    }
    
    // 複数の非同期処理の組み合わせ
    public static void combinationExample() {
        CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
            simulateDelay(500);
            return "ユーザー: 田中";
        });
        
        CompletableFuture<String> orderFuture = CompletableFuture.supplyAsync(() -> {
            simulateDelay(800);
            return "注文: #12345";
        });
        
        // 2つのFutureの結果を組み合わせ
        CompletableFuture<String> combined = userFuture.thenCombine(
            orderFuture,
            (user, order) -> user + ", " + order
        );
        
        // すべて完了を待つ
        CompletableFuture<Void> allOf = CompletableFuture.allOf(
            userFuture, orderFuture
        );
        
        // いずれか一つの完了を待つ
        CompletableFuture<Object> anyOf = CompletableFuture.anyOf(
            userFuture, orderFuture
        );
    }
    
    // エラーハンドリング
    public static void errorHandling() {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            if (true) throw new RuntimeException("エラー発生");
            return "成功";
        });
        
        // exceptionally: エラー時のフォールバック
        CompletableFuture<String> recovered = future
            .exceptionally(ex -> "エラー回復: " + ex.getMessage());
        
        // handle: 成功・失敗の両方を処理
        CompletableFuture<String> handled = future
            .handle((result, ex) -> {
                if (ex != null) return "エラー: " + ex.getMessage();
                return "成功: " + result;
            });
        
        // whenComplete: 結果を変えずに副作用を実行
        future.whenComplete((result, ex) -> {
            if (ex != null) {
                System.err.println("失敗: " + ex.getMessage());
            } else {
                System.out.println("成功: " + result);
            }
        });
    }
    
    // 実践例: 複数APIの並列呼び出し
    public static CompletableFuture<UserProfile> fetchUserProfile(String userId) {
        CompletableFuture<User> userFuture = 
            CompletableFuture.supplyAsync(() -> fetchUser(userId));
        CompletableFuture<List<Order>> ordersFuture = 
            CompletableFuture.supplyAsync(() -> fetchOrders(userId));
        CompletableFuture<Address> addressFuture = 
            CompletableFuture.supplyAsync(() -> fetchAddress(userId));
        
        return userFuture.thenCombine(ordersFuture, (user, orders) -> {
            user.setOrders(orders);
            return user;
        }).thenCombine(addressFuture, (user, address) -> {
            user.setAddress(address);
            return new UserProfile(user);
        }).orTimeout(5, TimeUnit.SECONDS);  // タイムアウト設定(Java 9+)
    }
    
    private static void simulateDelay(long ms) {
        try { Thread.sleep(ms); } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

並行コレクション

// ConcurrentHashMap: スレッドセーフなHashMap
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("key1", 1);
concurrentMap.putIfAbsent("key2", 2);
concurrentMap.compute("key1", (k, v) -> v + 10);  // アトミック操作

// CopyOnWriteArrayList: 読み取りが多い場合に適する
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("item1");
// 書き込み時に内部配列全体をコピーするため、書き込みは遅い

// BlockingQueue: プロデューサー・コンシューマーパターン
BlockingQueue<String> queue = new LinkedBlockingQueue<>(10);
// プロデューサー
queue.put("タスク");        // キューが満杯なら待機
queue.offer("タスク", 1, TimeUnit.SECONDS);  // タイムアウト付き

// コンシューマー
String task = queue.take();  // キューが空なら待機

// AtomicInteger: ロックフリーのアトミック操作
AtomicInteger atomicCount = new AtomicInteger(0);
atomicCount.incrementAndGet();     // ++count
atomicCount.getAndIncrement();     // count++
atomicCount.compareAndSet(1, 2);   // CAS操作
atomicCount.updateAndGet(n -> n * 2);  // アトミックな更新

Virtual Threads(Project Loom)

Java 21で正式導入されたVirtual Threadsは、軽量なスレッドであり、大量のI/O待ちスレッドを効率的に管理できる。

// Virtual Threadの作成
Thread vThread = Thread.ofVirtual()
    .name("virtual-", 0)
    .start(() -> {
        System.out.println("Virtual Thread: " + Thread.currentThread());
    });

// Virtual Threadのファクトリ
ThreadFactory factory = Thread.ofVirtual()
    .name("worker-", 0)
    .factory();

// Virtual Thread用ExecutorService
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    // 100万個のVirtual Threadを起動(プラットフォームスレッドでは不可能)
    List<Future<String>> futures = new ArrayList<>();
    for (int i = 0; i < 1_000_000; i++) {
        final int taskId = i;
        futures.add(executor.submit(() -> {
            Thread.sleep(1000);  // I/O待ちをシミュレート
            return "タスク" + taskId + "完了";
        }));
    }
    
    // 結果の収集
    for (Future<String> future : futures) {
        future.get();
    }
}  // try-with-resourcesで自動シャットダウン

// Virtual Threads vs Platform Threads
// - Virtual Threads: JVMが管理する軽量スレッド。数百万個作成可能
// - Platform Threads: OSスレッドと1:1対応。数千個が限界
// - I/Oバウンドな処理に最適(HTTPリクエスト、DB接続、ファイルI/O)
// - CPUバウンドな処理にはプラットフォームスレッドの方が適切

// Structured Concurrency(プレビュー機能)
// タスクのライフサイクルを構造化して管理
/*
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Subtask<String> user = scope.fork(() -> fetchUser(userId));
    Subtask<String> order = scope.fork(() -> fetchOrder(orderId));
    
    scope.join();           // 全サブタスクの完了を待つ
    scope.throwIfFailed();  // エラーがあれば例外をスロー
    
    return new Response(user.get(), order.get());
}
*/

Java I/Oとネットワーキング

ファイルI/O(java.io)

java.ioパッケージは、ストリームベースのI/O操作を提供する。

import java.io.*;

public class TraditionalIO {
    
    // バイトストリーム
    public void copyFile(String src, String dst) throws IOException {
        try (InputStream in = new BufferedInputStream(new FileInputStream(src));
             OutputStream out = new BufferedOutputStream(new FileOutputStream(dst))) {
            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
        }
    }
    
    // 文字ストリーム
    public void readTextFile(String path) throws IOException {
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(
                    new FileInputStream(path), StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        }
    }
    
    // テキストファイルへの書き込み
    public void writeTextFile(String path, List<String> lines) throws IOException {
        try (PrintWriter writer = new PrintWriter(
                new BufferedWriter(
                    new OutputStreamWriter(
                        new FileOutputStream(path), StandardCharsets.UTF_8)))) {
            for (String line : lines) {
                writer.println(line);
            }
        }
    }
}

NIO.2(java.nio.file)

Java 7で導入されたNIO.2は、より現代的で柔軟なファイルI/O APIを提供する。

import java.nio.file.*;
import java.nio.charset.StandardCharsets;

public class NIO2Example {
    
    // Pathの操作
    public void pathOperations() {
        Path path = Path.of("/home", "user", "documents", "report.txt");
        // または: Paths.get("/home/user/documents/report.txt")
        
        System.out.println("ファイル名: " + path.getFileName());       // report.txt
        System.out.println("親: " + path.getParent());                 // /home/user/documents
        System.out.println("ルート: " + path.getRoot());               // /
        System.out.println("絶対パス: " + path.toAbsolutePath());
        System.out.println("正規化: " + path.normalize());
        
        // パスの結合
        Path base = Path.of("/home/user");
        Path resolved = base.resolve("documents/report.txt");
        
        // 相対パスの取得
        Path relative = base.relativize(path);  // documents/report.txt
    }
    
    // ファイルの読み書き(簡潔なAPI)
    public void simpleReadWrite() throws IOException {
        Path file = Path.of("example.txt");
        
        // 全行を読み込み
        List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8);
        
        // 全バイトを読み込み
        byte[] bytes = Files.readAllBytes(file);
        
        // 文字列として読み込み(Java 11+)
        String content = Files.readString(file, StandardCharsets.UTF_8);
        
        // ファイルへの書き込み
        Files.writeString(
            Path.of("output.txt"),
            "Hello, NIO.2!",
            StandardCharsets.UTF_8,
            StandardOpenOption.CREATE,
            StandardOpenOption.TRUNCATE_EXISTING
        );
        
        // 行単位での書き込み
        Files.write(
            Path.of("output.txt"),
            List.of("行1", "行2", "行3"),
            StandardCharsets.UTF_8
        );
        
        // Streamとして読み込み(大きなファイル向け、遅延読み込み)
        try (Stream<String> stream = Files.lines(file, StandardCharsets.UTF_8)) {
            stream.filter(line -> !line.isBlank())
                  .map(String::trim)
                  .forEach(System.out::println);
        }
    }
    
    // ファイル・ディレクトリ操作
    public void fileOperations() throws IOException {
        Path dir = Path.of("testdir");
        Path file = dir.resolve("test.txt");
        
        // ディレクトリ作成
        Files.createDirectories(dir);
        
        // ファイル作成
        Files.createFile(file);
        
        // ファイルコピー
        Files.copy(file, Path.of("backup.txt"), 
            StandardCopyOption.REPLACE_EXISTING);
        
        // ファイル移動
        Files.move(file, dir.resolve("renamed.txt"),
            StandardCopyOption.ATOMIC_MOVE);
        
        // ファイル削除
        Files.deleteIfExists(dir.resolve("renamed.txt"));
        
        // ファイル属性
        BasicFileAttributes attrs = Files.readAttributes(
            Path.of("example.txt"), BasicFileAttributes.class);
        System.out.println("サイズ: " + attrs.size());
        System.out.println("作成日時: " + attrs.creationTime());
        System.out.println("最終更新: " + attrs.lastModifiedTime());
        System.out.println("ディレクトリ: " + attrs.isDirectory());
        
        // ファイル存在チェック
        boolean exists = Files.exists(Path.of("example.txt"));
        boolean isRegular = Files.isRegularFile(Path.of("example.txt"));
    }
    
    // ディレクトリ走査
    public void directoryTraversal() throws IOException {
        Path dir = Path.of("/home/user/projects");
        
        // 直下のエントリのみ
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.java")) {
            for (Path entry : stream) {
                System.out.println(entry.getFileName());
            }
        }
        
        // 再帰的な走査
        try (Stream<Path> walk = Files.walk(dir)) {
            List<Path> javaFiles = walk
                .filter(p -> p.toString().endsWith(".java"))
                .toList();
        }
        
        // find: 条件付き検索
        try (Stream<Path> found = Files.find(dir, 10,
                (path, attrs) -> attrs.isRegularFile() 
                    && path.toString().endsWith(".java"))) {
            found.forEach(System.out::println);
        }
    }
    
    // WatchService: ファイルシステムの変更監視
    public void watchDirectory() throws IOException, InterruptedException {
        WatchService watcher = FileSystems.getDefault().newWatchService();
        Path dir = Path.of("watched-dir");
        
        dir.register(watcher,
            StandardWatchEventKinds.ENTRY_CREATE,
            StandardWatchEventKinds.ENTRY_MODIFY,
            StandardWatchEventKinds.ENTRY_DELETE);
        
        while (true) {
            WatchKey key = watcher.take();  // ブロッキング
            for (WatchEvent<?> event : key.pollEvents()) {
                WatchEvent.Kind<?> kind = event.kind();
                Path filename = (Path) event.context();
                System.out.printf("イベント: %s, ファイル: %s%n", kind, filename);
            }
            if (!key.reset()) break;
        }
    }
}

ソケットプログラミング

// TCPサーバー
public class SimpleTcpServer {
    public void start(int port) throws IOException {
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("サーバー起動: ポート " + port);
            
            while (true) {
                Socket clientSocket = serverSocket.accept();
                // クライアントごとにスレッドを生成
                Thread.ofVirtual().start(() -> handleClient(clientSocket));
            }
        }
    }
    
    private void handleClient(Socket socket) {
        try (socket;
             var reader = new BufferedReader(
                 new InputStreamReader(socket.getInputStream()));
             var writer = new PrintWriter(socket.getOutputStream(), true)) {
            
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println("受信: " + line);
                writer.println("エコー: " + line);
            }
        } catch (IOException e) {
            System.err.println("クライアントエラー: " + e.getMessage());
        }
    }
}

// TCPクライアント
public class SimpleTcpClient {
    public void connect(String host, int port) throws IOException {
        try (Socket socket = new Socket(host, port);
             var writer = new PrintWriter(socket.getOutputStream(), true);
             var reader = new BufferedReader(
                 new InputStreamReader(socket.getInputStream()))) {
            
            writer.println("Hello, Server!");
            String response = reader.readLine();
            System.out.println("応答: " + response);
        }
    }
}

HTTP Client(java.net.http)

Java 11で導入された標準HTTPクライアントAPI。

import java.net.http.*;
import java.net.URI;

public class HttpClientExample {
    
    private final HttpClient client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_2)
        .connectTimeout(Duration.ofSeconds(10))
        .followRedirects(HttpClient.Redirect.NORMAL)
        .build();
    
    // 同期GETリクエスト
    public String get(String url) throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .header("Accept", "application/json")
            .GET()
            .build();
        
        HttpResponse<String> response = client.send(
            request, HttpResponse.BodyHandlers.ofString());
        
        System.out.println("ステータス: " + response.statusCode());
        System.out.println("ヘッダー: " + response.headers().map());
        return response.body();
    }
    
    // 非同期GETリクエスト
    public CompletableFuture<String> getAsync(String url) {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .GET()
            .build();
        
        return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
            .thenApply(HttpResponse::body);
    }
    
    // POSTリクエスト
    public String post(String url, String jsonBody) 
            throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
            .timeout(Duration.ofSeconds(30))
            .build();
        
        HttpResponse<String> response = client.send(
            request, HttpResponse.BodyHandlers.ofString());
        return response.body();
    }
    
    // ファイルダウンロード
    public Path download(String url, String savePath) 
            throws IOException, InterruptedException {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .GET()
            .build();
        
        HttpResponse<Path> response = client.send(
            request, HttpResponse.BodyHandlers.ofFile(Path.of(savePath)));
        return response.body();
    }
    
    // 複数の非同期リクエスト
    public List<String> fetchMultiple(List<String> urls) {
        List<CompletableFuture<String>> futures = urls.stream()
            .map(this::getAsync)
            .toList();
        
        return futures.stream()
            .map(CompletableFuture::join)
            .toList();
    }
}

シリアライゼーション

// Javaシリアライゼーション(レガシー、セキュリティ上推奨されない)
public class User implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    
    private String name;
    private transient String password;  // transientフィールドはシリアライズされない
    private int age;
    
    // コンストラクタ、getter, setter省略
}

// シリアライズ
try (ObjectOutputStream oos = new ObjectOutputStream(
        new FileOutputStream("user.ser"))) {
    oos.writeObject(new User("田中", "secret", 30));
}

// デシリアライズ
try (ObjectInputStream ois = new ObjectInputStream(
        new FileInputStream("user.ser"))) {
    User user = (User) ois.readObject();
}

// 推奨: JSON(Jackson)を使用
// ObjectMapper mapper = new ObjectMapper();
// String json = mapper.writeValueAsString(user);
// User deserialized = mapper.readValue(json, User.class);

データベースアクセス(JDBC/JPA)

JDBCの基本

JDBC(Java Database Connectivity)は、Javaからデータベースにアクセスするための標準APIである。

基本的なJDBC操作

import java.sql.*;

public class JdbcExample {
    
    private static final String URL = "jdbc:mysql://localhost:3306/mydb";
    private static final String USER = "dbuser";
    private static final String PASSWORD = "dbpassword";
    
    // 接続とクエリ
    public List<User> findAllUsers() throws SQLException {
        List<User> users = new ArrayList<>();
        String sql = "SELECT id, name, email, created_at FROM users ORDER BY id";
        
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {
            
            while (rs.next()) {
                User user = new User(
                    rs.getLong("id"),
                    rs.getString("name"),
                    rs.getString("email"),
                    rs.getTimestamp("created_at").toLocalDateTime()
                );
                users.add(user);
            }
        }
        return users;
    }
    
    // PreparedStatement(SQLインジェクション対策)
    public User findUserById(long id) throws SQLException {
        String sql = "SELECT id, name, email, created_at FROM users WHERE id = ?";
        
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            
            pstmt.setLong(1, id);  // パラメータバインド
            
            try (ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    return new User(
                        rs.getLong("id"),
                        rs.getString("name"),
                        rs.getString("email"),
                        rs.getTimestamp("created_at").toLocalDateTime()
                    );
                }
            }
        }
        return null;
    }
    
    // INSERT操作
    public long createUser(String name, String email) throws SQLException {
        String sql = "INSERT INTO users (name, email, created_at) VALUES (?, ?, NOW())";
        
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             PreparedStatement pstmt = conn.prepareStatement(sql, 
                 Statement.RETURN_GENERATED_KEYS)) {
            
            pstmt.setString(1, name);
            pstmt.setString(2, email);
            pstmt.executeUpdate();
            
            try (ResultSet keys = pstmt.getGeneratedKeys()) {
                if (keys.next()) {
                    return keys.getLong(1);
                }
            }
        }
        return -1;
    }
    
    // バッチ処理
    public void batchInsert(List<User> users) throws SQLException {
        String sql = "INSERT INTO users (name, email, created_at) VALUES (?, ?, NOW())";
        
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            
            conn.setAutoCommit(false);  // トランザクション開始
            
            try {
                for (User user : users) {
                    pstmt.setString(1, user.getName());
                    pstmt.setString(2, user.getEmail());
                    pstmt.addBatch();
                }
                
                int[] results = pstmt.executeBatch();
                conn.commit();  // コミット
                System.out.println("挿入件数: " + results.length);
            } catch (SQLException e) {
                conn.rollback();  // ロールバック
                throw e;
            }
        }
    }
    
    // トランザクション管理
    public void transferFunds(long fromId, long toId, double amount) 
            throws SQLException {
        String debit = "UPDATE accounts SET balance = balance - ? WHERE id = ?";
        String credit = "UPDATE accounts SET balance = balance + ? WHERE id = ?";
        
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD)) {
            conn.setAutoCommit(false);
            conn.setTransactionIsolation(
                Connection.TRANSACTION_READ_COMMITTED);
            
            try (PreparedStatement debitStmt = conn.prepareStatement(debit);
                 PreparedStatement creditStmt = conn.prepareStatement(credit)) {
                
                debitStmt.setDouble(1, amount);
                debitStmt.setLong(2, fromId);
                int debitRows = debitStmt.executeUpdate();
                
                creditStmt.setDouble(1, amount);
                creditStmt.setLong(2, toId);
                int creditRows = creditStmt.executeUpdate();
                
                if (debitRows == 1 && creditRows == 1) {
                    conn.commit();
                } else {
                    conn.rollback();
                    throw new SQLException("振込に失敗しました");
                }
            } catch (SQLException e) {
                conn.rollback();
                throw e;
            }
        }
    }
}

コネクションプール(HikariCP)

本番環境では、DB接続のオーバーヘッドを軽減するためにコネクションプールを使用する。HikariCPは最も高速なコネクションプール実装である。

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

public class ConnectionPoolExample {
    
    private static HikariDataSource dataSource;
    
    static {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
        config.setUsername("dbuser");
        config.setPassword("dbpassword");
        
        // プール設定
        config.setMaximumPoolSize(10);        // 最大接続数
        config.setMinimumIdle(5);             // 最小アイドル接続数
        config.setConnectionTimeout(30000);   // 接続取得タイムアウト(30秒)
        config.setIdleTimeout(600000);        // アイドルタイムアウト(10分)
        config.setMaxLifetime(1800000);       // 最大生存時間(30分)
        
        // パフォーマンス設定
        config.addDataSourceProperty("cachePrepStmts", "true");
        config.addDataSourceProperty("prepStmtCacheSize", "250");
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");
        config.addDataSourceProperty("useServerPrepStmts", "true");
        
        dataSource = new HikariDataSource(config);
    }
    
    public static Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }
    
    // 使用例
    public List<User> findUsers() throws SQLException {
        try (Connection conn = getConnection();
             PreparedStatement pstmt = conn.prepareStatement(
                 "SELECT * FROM users WHERE active = ?")) {
            pstmt.setBoolean(1, true);
            try (ResultSet rs = pstmt.executeQuery()) {
                List<User> users = new ArrayList<>();
                while (rs.next()) {
                    // マッピング処理
                }
                return users;
            }
        }
    }
}

JPA/Hibernateの基本

JPA(Java Persistence API)は、オブジェクトとリレーショナルデータベース間のマッピング(ORM)を提供する標準仕様である。Hibernateはその最も広く使われている実装である。

エンティティマッピング

import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.List;

@Entity
@Table(name = "users")
public class UserEntity {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, length = 100)
    private String name;
    
    @Column(unique = true, nullable = false)
    private String email;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private UserStatus status = UserStatus.ACTIVE;
    
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    // 一対多リレーション
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, 
               orphanRemoval = true, fetch = FetchType.LAZY)
    private List<OrderEntity> orders = new ArrayList<>();
    
    // 多対一リレーション(Order側)
    // @ManyToOne(fetch = FetchType.LAZY)
    // @JoinColumn(name = "user_id", nullable = false)
    // private UserEntity user;
    
    // 多対多リレーション
    @ManyToMany
    @JoinTable(
        name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<RoleEntity> roles = new HashSet<>();
    
    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }
    
    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }
    
    // コンストラクタ、getter, setter省略
}

public enum UserStatus {
    ACTIVE, INACTIVE, SUSPENDED
}

JPAリポジトリ(EntityManager)

@Repository
public class UserRepository {
    
    @PersistenceContext
    private EntityManager em;
    
    // 保存
    @Transactional
    public UserEntity save(UserEntity user) {
        if (user.getId() == null) {
            em.persist(user);
            return user;
        } else {
            return em.merge(user);
        }
    }
    
    // ID検索
    public Optional<UserEntity> findById(Long id) {
        return Optional.ofNullable(em.find(UserEntity.class, id));
    }
    
    // JPQL(Java Persistence Query Language)
    public List<UserEntity> findByStatus(UserStatus status) {
        return em.createQuery(
            "SELECT u FROM UserEntity u WHERE u.status = :status ORDER BY u.name",
            UserEntity.class)
            .setParameter("status", status)
            .getResultList();
    }
    
    // ページネーション
    public List<UserEntity> findAll(int page, int size) {
        return em.createQuery("SELECT u FROM UserEntity u ORDER BY u.id",
            UserEntity.class)
            .setFirstResult(page * size)
            .setMaxResults(size)
            .getResultList();
    }
    
    // Criteria API(型安全なクエリ構築)
    public List<UserEntity> searchUsers(String keyword, UserStatus status) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<UserEntity> query = cb.createQuery(UserEntity.class);
        Root<UserEntity> root = query.from(UserEntity.class);
        
        List<Predicate> predicates = new ArrayList<>();
        
        if (keyword != null && !keyword.isBlank()) {
            predicates.add(cb.or(
                cb.like(cb.lower(root.get("name")), 
                    "%" + keyword.toLowerCase() + "%"),
                cb.like(cb.lower(root.get("email")), 
                    "%" + keyword.toLowerCase() + "%")
            ));
        }
        
        if (status != null) {
            predicates.add(cb.equal(root.get("status"), status));
        }
        
        query.where(predicates.toArray(new Predicate[0]));
        query.orderBy(cb.asc(root.get("name")));
        
        return em.createQuery(query).getResultList();
    }
    
    // 削除
    @Transactional
    public void deleteById(Long id) {
        UserEntity user = em.find(UserEntity.class, id);
        if (user != null) {
            em.remove(user);
        }
    }
}

persistence.xml設定例

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             version="3.0">
    
    <persistence-unit name="myPU" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        
        <properties>
            <!-- データソース -->
            <property name="jakarta.persistence.jdbc.url" 
                      value="jdbc:mysql://localhost:3306/mydb"/>
            <property name="jakarta.persistence.jdbc.user" value="dbuser"/>
            <property name="jakarta.persistence.jdbc.password" value="dbpassword"/>
            <property name="jakarta.persistence.jdbc.driver" 
                      value="com.mysql.cj.jdbc.Driver"/>
            
            <!-- Hibernate設定 -->
            <property name="hibernate.dialect" 
                      value="org.hibernate.dialect.MySQLDialect"/>
            <property name="hibernate.hbm2ddl.auto" value="validate"/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            
            <!-- 二次キャッシュ -->
            <property name="hibernate.cache.use_second_level_cache" value="true"/>
            <property name="hibernate.cache.region.factory_class" 
                      value="org.hibernate.cache.jcache.JCacheRegionFactory"/>
        </properties>
    </persistence-unit>
</persistence>

Spring Framework / Spring Boot

Spring Frameworkの概要

Spring Frameworkは、Javaエンタープライズアプリケーション開発のデファクトスタンダードとなるフレームワークである。2003年にRod Johnsonによって開発が開始され、EJB(Enterprise JavaBeans)の複雑さに対するアンチテーゼとして生まれた。

DI(Dependency Injection)/ IoC(Inversion of Control)

DIは、オブジェクトの依存関係をコンテナが外部から注入する設計パターンである。

// インターフェース
public interface NotificationService {
    void send(String to, String message);
}

// 実装クラス
@Service
public class EmailNotificationService implements NotificationService {
    private final JavaMailSender mailSender;
    
    // コンストラクタインジェクション(推奨)
    public EmailNotificationService(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }
    
    @Override
    public void send(String to, String message) {
        // メール送信ロジック
        SimpleMailMessage mail = new SimpleMailMessage();
        mail.setTo(to);
        mail.setSubject("通知");
        mail.setText(message);
        mailSender.send(mail);
    }
}

// 使用側: NotificationServiceの具体的な実装を知らない
@Service
public class OrderService {
    private final NotificationService notificationService;
    private final OrderRepository orderRepository;
    
    // Springが自動的にEmailNotificationServiceを注入
    public OrderService(NotificationService notificationService,
                       OrderRepository orderRepository) {
        this.notificationService = notificationService;
        this.orderRepository = orderRepository;
    }
    
    @Transactional
    public Order createOrder(OrderRequest request) {
        Order order = new Order(request);
        orderRepository.save(order);
        notificationService.send(
            request.getEmail(), "注文 #" + order.getId() + " を受け付けました");
        return order;
    }
}

AOP(Aspect-Oriented Programming)

横断的関心事(ロギング、トランザクション管理、セキュリティなど)を本来のビジネスロジックから分離する。

@Aspect
@Component
public class LoggingAspect {
    
    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
    
    // メソッド実行前後にログを出力
    @Around("@annotation(Loggable)")
    public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().toShortString();
        Object[] args = joinPoint.getArgs();
        
        log.info("開始: {} 引数: {}", methodName, Arrays.toString(args));
        long start = System.currentTimeMillis();
        
        try {
            Object result = joinPoint.proceed();
            long elapsed = System.currentTimeMillis() - start;
            log.info("完了: {} 実行時間: {}ms", methodName, elapsed);
            return result;
        } catch (Exception e) {
            log.error("エラー: {} 例外: {}", methodName, e.getMessage());
            throw e;
        }
    }
    
    // 特定パッケージの全メソッドに適用
    @Before("execution(* com.example.service.*.*(..))")
    public void logServiceCall(JoinPoint joinPoint) {
        log.debug("サービス呼び出し: {}", joinPoint.getSignature().getName());
    }
}

// カスタムアノテーション
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {}

Spring Bootのアーキテクチャ

Spring Bootは、Spring Frameworkの設定を大幅に簡素化し、「Convention over Configuration(設定より規約)」の思想に基づく。

プロジェクト構造

src/
├── main/
│   ├── java/com/example/demo/
│   │   ├── DemoApplication.java          # メインクラス
│   │   ├── config/                       # 設定クラス
│   │   │   ├── SecurityConfig.java
│   │   │   └── WebConfig.java
│   │   ├── controller/                   # コントローラー層
│   │   │   └── UserController.java
│   │   ├── service/                      # サービス層
│   │   │   ├── UserService.java
│   │   │   └── UserServiceImpl.java
│   │   ├── repository/                   # リポジトリ層
│   │   │   └── UserRepository.java
│   │   ├── entity/                       # エンティティ
│   │   │   └── User.java
│   │   ├── dto/                          # データ転送オブジェクト
│   │   │   ├── UserRequest.java
│   │   │   └── UserResponse.java
│   │   └── exception/                    # 例外処理
│   │       ├── GlobalExceptionHandler.java
│   │       └── ResourceNotFoundException.java
│   └── resources/
│       ├── application.yml               # メイン設定
│       ├── application-dev.yml           # 開発環境設定
│       ├── application-prod.yml          # 本番環境設定
│       └── db/migration/                 # Flywayマイグレーション
│           └── V1__create_users_table.sql
└── test/
    └── java/com/example/demo/
        ├── controller/
        │   └── UserControllerTest.java
        └── service/
            └── UserServiceTest.java

メインクラス

@SpringBootApplication  // @Configuration + @EnableAutoConfiguration + @ComponentScan
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

REST APIの構築

// DTO
public record UserRequest(
    @NotBlank(message = "名前は必須です")
    String name,
    
    @Email(message = "有効なメールアドレスを入力してください")
    @NotBlank
    String email,
    
    @Min(value = 0, message = "年齢は0以上でなければなりません")
    Integer age
) {}

public record UserResponse(
    Long id,
    String name,
    String email,
    Integer age,
    LocalDateTime createdAt
) {
    public static UserResponse from(User user) {
        return new UserResponse(
            user.getId(), user.getName(), user.getEmail(),
            user.getAge(), user.getCreatedAt());
    }
}

// コントローラー
@RestController
@RequestMapping("/api/v1/users")
@Validated
public class UserController {
    
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @GetMapping
    public ResponseEntity<List<UserResponse>> getAllUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        List<UserResponse> users = userService.findAll(page, size);
        return ResponseEntity.ok(users);
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<UserResponse> getUserById(@PathVariable Long id) {
        UserResponse user = userService.findById(id);
        return ResponseEntity.ok(user);
    }
    
    @PostMapping
    public ResponseEntity<UserResponse> createUser(
            @Valid @RequestBody UserRequest request) {
        UserResponse created = userService.create(request);
        URI location = URI.create("/api/v1/users/" + created.id());
        return ResponseEntity.created(location).body(created);
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<UserResponse> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody UserRequest request) {
        UserResponse updated = userService.update(id, request);
        return ResponseEntity.ok(updated);
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

// グローバル例外ハンドラー
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException e) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            e.getMessage(),
            LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidation(
            MethodArgumentNotValidException e) {
        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getFieldErrors().forEach(error ->
            errors.put(error.getField(), error.getDefaultMessage())
        );
        
        ErrorResponse response = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "バリデーションエラー",
            LocalDateTime.now(),
            errors
        );
        return ResponseEntity.badRequest().body(response);
    }
}

Spring Data JPA

// Spring Data JPAリポジトリ
public interface UserRepository extends JpaRepository<User, Long> {
    
    // メソッド名から自動的にクエリを生成
    List<User> findByNameContaining(String keyword);
    Optional<User> findByEmail(String email);
    List<User> findByAgeGreaterThanEqualOrderByNameAsc(int age);
    boolean existsByEmail(String email);
    
    // カスタムクエリ
    @Query("SELECT u FROM User u WHERE u.status = :status AND u.age >= :minAge")
    List<User> findActiveUsersAboveAge(
        @Param("status") UserStatus status, 
        @Param("minAge") int minAge);
    
    // ネイティブクエリ
    @Query(value = "SELECT * FROM users WHERE email LIKE %:domain", 
           nativeQuery = true)
    List<User> findByEmailDomain(@Param("domain") String domain);
    
    // ページネーション
    Page<User> findByStatus(UserStatus status, Pageable pageable);
}

// サービス層
@Service
@Transactional(readOnly = true)
public class UserServiceImpl implements UserService {
    
    private final UserRepository userRepository;
    
    public UserServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Override
    public List<UserResponse> findAll(int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("name"));
        return userRepository.findAll(pageable)
            .map(UserResponse::from)
            .getContent();
    }
    
    @Override
    @Transactional
    public UserResponse create(UserRequest request) {
        if (userRepository.existsByEmail(request.email())) {
            throw new DuplicateResourceException("メールアドレスは既に使用されています");
        }
        User user = new User(request.name(), request.email(), request.age());
        User saved = userRepository.save(user);
        return UserResponse.from(saved);
    }
}

application.yml設定例

# application.yml
spring:
  application:
    name: demo-service
  
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:dev}
  
  datasource:
    url: jdbc:mysql://localhost:3306/mydb?useSSL=true&serverTimezone=Asia/Tokyo
    username: ${DB_USERNAME:dbuser}
    password: ${DB_PASSWORD:dbpassword}
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
  
  jpa:
    hibernate:
      ddl-auto: validate
    properties:
      hibernate:
        format_sql: true
        default_batch_fetch_size: 100
    open-in-view: false
  
  jackson:
    serialization:
      write-dates-as-timestamps: false
    deserialization:
      fail-on-unknown-properties: false

server:
  port: 8080
  servlet:
    context-path: /api

logging:
  level:
    root: INFO
    com.example: DEBUG
    org.hibernate.SQL: DEBUG

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: when-authorized

---
# application-prod.yml
spring:
  config:
    activate:
      on-profile: prod
  
  datasource:
    url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}
    hikari:
      maximum-pool-size: 20

server:
  port: ${SERVER_PORT:8080}

logging:
  level:
    root: WARN
    com.example: INFO

Spring Securityの基本

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/v1/auth/**").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/v1/public/**").permitAll()
                .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthFilter(), 
                UsernamePasswordAuthenticationFilter.class)
            .build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

テスト

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Test
    void createUser_shouldReturn201() throws Exception {
        UserRequest request = new UserRequest("田中太郎", "tanaka@example.com", 30);
        UserResponse response = new UserResponse(
            1L, "田中太郎", "tanaka@example.com", 30, LocalDateTime.now());
        
        when(userService.create(any())).thenReturn(response);
        
        mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.name").value("田中太郎"))
            .andExpect(jsonPath("$.email").value("tanaka@example.com"));
    }
    
    @Test
    void createUser_withInvalidEmail_shouldReturn400() throws Exception {
        UserRequest request = new UserRequest("田中", "invalid-email", 30);
        
        mockMvc.perform(post("/api/v1/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isBadRequest());
    }
}

ビルドツールと依存関係管理

Maven

Mavenは、Apache Software Foundationが開発したJava向けビルドツールであり、プロジェクトのビルド、依存関係管理、プロジェクト情報の管理を行う。XMLベースの設定ファイル(pom.xml)を使用する。

pom.xml の基本構造

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <!-- Spring Boot親POM -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/>
    </parent>
    
    <!-- プロジェクト座標 -->
    <groupId>com.example</groupId>
    <artifactId>demo-application</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    
    <name>Demo Application</name>
    <description>Spring Boot Demo</description>
    
    <!-- プロパティ -->
    <properties>
        <java.version>21</java.version>
        <mapstruct.version>1.5.5.Final</mapstruct.version>
        <testcontainers.version>1.19.3</testcontainers.version>
    </properties>
    
    <!-- 依存関係 -->
    <dependencies>
        <!-- Spring Boot Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        
        <!-- データベース -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        
        <!-- Flyway -->
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-mysql</artifactId>
        </dependency>
        
        <!-- ユーティリティ -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${mapstruct.version}</version>
        </dependency>
        
        <!-- テスト -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>mysql</artifactId>
            <version>${testcontainers.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <!-- ビルド設定 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${lombok.version}</version>
                        </path>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${mapstruct.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
    
    <!-- プロファイル -->
    <profiles>
        <profile>
            <id>prod</id>
            <properties>
                <spring.profiles.active>prod</spring.profiles.active>
            </properties>
        </profile>
    </profiles>
</project>

Mavenライフサイクル

# 主要なMavenコマンド
mvn clean                  # targetディレクトリを削除
mvn compile                # ソースコードをコンパイル
mvn test                   # テストを実行
mvn package                # JAR/WARを作成
mvn verify                 # 統合テストを実行
mvn install                # ローカルリポジトリにインストール
mvn deploy                 # リモートリポジトリにデプロイ

# よく使う組み合わせ
mvn clean package -DskipTests    # テストをスキップしてビルド
mvn clean verify                 # クリーンビルド+統合テスト
mvn dependency:tree              # 依存関係ツリーを表示
mvn versions:display-dependency-updates  # 更新可能な依存関係を表示

Gradle

Gradleは、GroovyまたはKotlin DSLを使用するビルドツールである。Mavenよりも柔軟で、ビルド速度が速い。

build.gradle.kts(Kotlin DSL)

plugins {
    java
    id("org.springframework.boot") version "3.2.0"
    id("io.spring.dependency-management") version "1.1.4"
    id("com.google.cloud.tools.jib") version "3.4.0"
    jacoco
}

group = "com.example"
version = "1.0.0-SNAPSHOT"

java {
    sourceCompatibility = JavaVersion.VERSION_21
    targetCompatibility = JavaVersion.VERSION_21
}

repositories {
    mavenCentral()
}

val testcontainersVersion = "1.19.3"
val mapstructVersion = "1.5.5.Final"

dependencies {
    // Spring Boot
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-validation")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-actuator")
    
    // データベース
    runtimeOnly("com.mysql:mysql-connector-j")
    implementation("org.flywaydb:flyway-core")
    implementation("org.flywaydb:flyway-mysql")
    
    // ユーティリティ
    compileOnly("org.projectlombok:lombok")
    annotationProcessor("org.projectlombok:lombok")
    implementation("org.mapstruct:mapstruct:$mapstructVersion")
    annotationProcessor("org.mapstruct:mapstruct-processor:$mapstructVersion")
    
    // テスト
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.testcontainers:mysql:$testcontainersVersion")
    testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion")
}

tasks.withType<Test> {
    useJUnitPlatform()
}

// JaCoCo設定
jacoco {
    toolVersion = "0.8.11"
}

tasks.jacocoTestReport {
    dependsOn(tasks.test)
    reports {
        xml.required.set(true)
        html.required.set(true)
    }
}

tasks.jacocoTestCoverageVerification {
    violationRules {
        rule {
            limit {
                minimum = "0.80".toBigDecimal()
            }
        }
    }
}

// Jib(Dockerイメージビルド)
jib {
    from {
        image = "eclipse-temurin:21-jre"
    }
    to {
        image = "registry.example.com/demo-app"
        tags = setOf("latest", version.toString())
    }
    container {
        jvmFlags = listOf(
            "-Xms512m",
            "-Xmx1g",
            "-XX:+UseG1GC"
        )
        ports = listOf("8080")
        mainClass = "com.example.demo.DemoApplication"
    }
}

Gradleコマンド

# 主要なGradleコマンド
./gradlew build            # ビルド(テスト含む)
./gradlew test             # テスト実行
./gradlew bootRun          # Spring Bootアプリケーション起動
./gradlew clean build      # クリーンビルド
./gradlew dependencies     # 依存関係ツリーを表示
./gradlew bootJar          # 実行可能JARを生成
./gradlew jib              # Dockerイメージをビルド&プッシュ
./gradlew jibDockerBuild   # ローカルDockerイメージをビルド

# Gradleラッパー(推奨)
# gradle/wrapper/gradle-wrapper.properties でバージョン管理

依存関係の管理とバージョン競合の解決

// build.gradle.kts
configurations.all {
    // 特定の依存関係を除外
    exclude(group = "commons-logging", module = "commons-logging")
    
    // キャッシュの有効期限
    resolutionStrategy.cacheChangingModulesFor(0, "seconds")
    
    // バージョン競合時の強制指定
    resolutionStrategy {
        force("com.google.guava:guava:32.1.3-jre")
    }
}

// BOM(Bill of Materials)の使用
dependencyManagement {
    imports {
        mavenBom("software.amazon.awssdk:bom:2.21.0")
        mavenBom("org.testcontainers:testcontainers-bom:1.19.3")
    }
}

マルチモジュールプロジェクト

// settings.gradle.kts(ルート)
rootProject.name = "multi-module-project"
include("common", "api", "batch", "infrastructure")

// build.gradle.kts(ルート)
plugins {
    java
    id("org.springframework.boot") version "3.2.0" apply false
    id("io.spring.dependency-management") version "1.1.4" apply false
}

subprojects {
    apply(plugin = "java")
    apply(plugin = "io.spring.dependency-management")
    
    group = "com.example"
    version = "1.0.0-SNAPSHOT"
    
    java {
        sourceCompatibility = JavaVersion.VERSION_21
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        testImplementation("org.junit.jupiter:junit-jupiter")
    }
    
    tasks.test {
        useJUnitPlatform()
    }
}

// api/build.gradle.kts
plugins {
    id("org.springframework.boot")
}

dependencies {
    implementation(project(":common"))
    implementation(project(":infrastructure"))
    implementation("org.springframework.boot:spring-boot-starter-web")
}

// common/build.gradle.kts
// 共通ライブラリ(Spring Boot pluginは不要)
dependencies {
    implementation("com.fasterxml.jackson.core:jackson-annotations")
}

テスティング

JUnit 5の基本

JUnit 5(JUnit Jupiter)は、Javaの標準テスティングフレームワークである。JUnit 5は、JUnit Platform、JUnit Jupiter、JUnit Vintageの3つのモジュールで構成される。

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    
    private Calculator calculator;
    
    @BeforeAll
    static void setUpAll() {
        System.out.println("全テスト開始前に1回だけ実行");
    }
    
    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }
    
    @Test
    @DisplayName("2つの正の整数の加算")
    void add_positiveNumbers_returnsSum() {
        assertEquals(5, calculator.add(2, 3));
    }
    
    @Test
    @DisplayName("ゼロ除算で例外がスローされること")
    void divide_byZero_throwsException() {
        ArithmeticException exception = assertThrows(
            ArithmeticException.class,
            () -> calculator.divide(10, 0)
        );
        assertEquals("/ by zero", exception.getMessage());
    }
    
    @Test
    @DisplayName("複数のアサーション")
    void multipleAssertions() {
        assertAll("計算結果の検証",
            () -> assertEquals(4, calculator.add(2, 2)),
            () -> assertEquals(0, calculator.add(-2, 2)),
            () -> assertEquals(-4, calculator.add(-2, -2))
        );
    }
    
    // パラメータ化テスト
    @ParameterizedTest
    @DisplayName("パラメータ化テスト: 加算")
    @CsvSource({
        "1, 1, 2",
        "2, 3, 5",
        "-1, 1, 0",
        "0, 0, 0",
        "100, -50, 50"
    })
    void add_variousInputs_returnsExpected(int a, int b, int expected) {
        assertEquals(expected, calculator.add(a, b));
    }
    
    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3, 5, 8, 13})
    void isPositive_positiveNumbers_returnsTrue(int number) {
        assertTrue(calculator.isPositive(number));
    }
    
    @ParameterizedTest
    @MethodSource("provideStringsForLength")
    void stringLength_variousStrings(String input, int expectedLength) {
        assertEquals(expectedLength, input.length());
    }
    
    static Stream<Arguments> provideStringsForLength() {
        return Stream.of(
            Arguments.of("Hello", 5),
            Arguments.of("", 0),
            Arguments.of("Java", 4)
        );
    }
    
    @ParameterizedTest
    @EnumSource(value = DayOfWeek.class, names = {"SATURDAY", "SUNDAY"})
    void isWeekend_weekendDays_returnsTrue(DayOfWeek day) {
        assertTrue(calculator.isWeekend(day));
    }
    
    // 条件付きテスト
    @Test
    @EnabledOnOs(OS.LINUX)
    void onlyOnLinux() {
        // Linuxのみで実行
    }
    
    @Test
    @EnabledIfEnvironmentVariable(named = "CI", matches = "true")
    void onlyOnCI() {
        // CI環境のみで実行
    }
    
    @Test
    @Timeout(value = 5, unit = TimeUnit.SECONDS)
    void shouldCompleteWithinTimeout() {
        // 5秒以内に完了しなければ失敗
    }
    
    // ネストテスト
    @Nested
    @DisplayName("減算のテスト")
    class SubtractionTests {
        @Test
        void subtract_positiveNumbers_returnsDifference() {
            assertEquals(2, calculator.subtract(5, 3));
        }
        
        @Test
        void subtract_negativeResult() {
            assertEquals(-3, calculator.subtract(2, 5));
        }
    }
    
    @AfterEach
    void tearDown() {
        calculator = null;
    }
    
    @AfterAll
    static void tearDownAll() {
        System.out.println("全テスト終了後に1回だけ実行");
    }
}

Mockitoによるモック

Mockitoは、モックオブジェクトを作成してユニットテストを容易にするフレームワークである。

import org.mockito.*;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    
    @Mock
    private UserRepository userRepository;
    
    @Mock
    private NotificationService notificationService;
    
    @InjectMocks
    private UserServiceImpl userService;
    
    @Captor
    private ArgumentCaptor<User> userCaptor;
    
    @Test
    @DisplayName("ユーザー作成: 正常系")
    void createUser_validInput_returnsCreatedUser() {
        // Arrange(準備)
        UserRequest request = new UserRequest("田中", "tanaka@example.com", 30);
        User savedUser = new User(1L, "田中", "tanaka@example.com", 30);
        
        when(userRepository.existsByEmail("tanaka@example.com")).thenReturn(false);
        when(userRepository.save(any(User.class))).thenReturn(savedUser);
        
        // Act(実行)
        UserResponse result = userService.create(request);
        
        // Assert(検証)
        assertNotNull(result);
        assertEquals("田中", result.name());
        assertEquals("tanaka@example.com", result.email());
        
        // リポジトリが正しい引数で呼ばれたか検証
        verify(userRepository).save(userCaptor.capture());
        User capturedUser = userCaptor.getValue();
        assertEquals("田中", capturedUser.getName());
        assertEquals("tanaka@example.com", capturedUser.getEmail());
        
        // 通知が送信されたか検証
        verify(notificationService).send(
            eq("tanaka@example.com"), 
            contains("アカウントが作成されました"));
        
        // 余分な呼び出しがないことを検証
        verifyNoMoreInteractions(notificationService);
    }
    
    @Test
    @DisplayName("ユーザー作成: メールアドレス重複")
    void createUser_duplicateEmail_throwsException() {
        UserRequest request = new UserRequest("田中", "tanaka@example.com", 30);
        when(userRepository.existsByEmail("tanaka@example.com")).thenReturn(true);
        
        assertThrows(DuplicateResourceException.class,
            () -> userService.create(request));
        
        // saveが呼ばれていないことを検証
        verify(userRepository, never()).save(any());
    }
    
    @Test
    @DisplayName("ユーザー検索: 存在しないID")
    void findById_nonExistentId_throwsNotFoundException() {
        when(userRepository.findById(999L)).thenReturn(Optional.empty());
        
        assertThrows(ResourceNotFoundException.class,
            () -> userService.findById(999L));
    }
    
    @Test
    @DisplayName("ユーザー一覧: ページネーション")
    void findAll_withPagination_returnsPaginatedResults() {
        // モックデータ
        List<User> users = List.of(
            new User(1L, "田中", "tanaka@example.com", 30),
            new User(2L, "佐藤", "sato@example.com", 25)
        );
        Page<User> page = new PageImpl<>(users, PageRequest.of(0, 10), 2);
        
        when(userRepository.findAll(any(Pageable.class))).thenReturn(page);
        
        List<UserResponse> result = userService.findAll(0, 10);
        
        assertEquals(2, result.size());
        verify(userRepository).findAll(argThat(
            (Pageable p) -> p.getPageNumber() == 0 && p.getPageSize() == 10
        ));
    }
}

インテグレーションテスト

// Testcontainersを使用したインテグレーションテスト
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@ActiveProfiles("test")
class UserIntegrationTest {
    
    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");
    
    @DynamicPropertySource
    static void mysqlProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", mysql::getJdbcUrl);
        registry.add("spring.datasource.username", mysql::getUsername);
        registry.add("spring.datasource.password", mysql::getPassword);
    }
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Autowired
    private UserRepository userRepository;
    
    @BeforeEach
    void setUp() {
        userRepository.deleteAll();
    }
    
    @Test
    @DisplayName("ユーザーCRUD操作のE2Eテスト")
    void userCrudOperations() {
        // CREATE
        UserRequest createRequest = new UserRequest("田中", "tanaka@test.com", 30);
        ResponseEntity<UserResponse> createResponse = restTemplate.postForEntity(
            "/api/v1/users", createRequest, UserResponse.class);
        
        assertEquals(HttpStatus.CREATED, createResponse.getStatusCode());
        assertNotNull(createResponse.getBody());
        Long userId = createResponse.getBody().id();
        
        // READ
        ResponseEntity<UserResponse> getResponse = restTemplate.getForEntity(
            "/api/v1/users/" + userId, UserResponse.class);
        
        assertEquals(HttpStatus.OK, getResponse.getStatusCode());
        assertEquals("田中", getResponse.getBody().name());
        
        // UPDATE
        UserRequest updateRequest = new UserRequest("田中太郎", "tanaka@test.com", 31);
        restTemplate.put("/api/v1/users/" + userId, updateRequest);
        
        ResponseEntity<UserResponse> updatedResponse = restTemplate.getForEntity(
            "/api/v1/users/" + userId, UserResponse.class);
        assertEquals("田中太郎", updatedResponse.getBody().name());
        
        // DELETE
        restTemplate.delete("/api/v1/users/" + userId);
        ResponseEntity<String> deletedResponse = restTemplate.getForEntity(
            "/api/v1/users/" + userId, String.class);
        assertEquals(HttpStatus.NOT_FOUND, deletedResponse.getStatusCode());
    }
}

テストカバレッジ(JaCoCo)

<!-- Maven: JaCoCo設定 -->
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.11</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
        <execution>
            <id>check</id>
            <goals>
                <goal>check</goal>
            </goals>
            <configuration>
                <rules>
                    <rule>
                        <element>BUNDLE</element>
                        <limits>
                            <limit>
                                <counter>LINE</counter>
                                <value>COVEREDRATIO</value>
                                <minimum>0.80</minimum>
                            </limit>
                            <limit>
                                <counter>BRANCH</counter>
                                <value>COVEREDRATIO</value>
                                <minimum>0.70</minimum>
                            </limit>
                        </limits>
                    </rule>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

TDD/BDDの実践

// BDDスタイルのテスト(BDDMockito)
import static org.mockito.BDDMockito.*;

@ExtendWith(MockitoExtension.class)
class OrderServiceBddTest {
    
    @Mock private OrderRepository orderRepository;
    @Mock private PaymentService paymentService;
    @Mock private InventoryService inventoryService;
    @InjectMocks private OrderService orderService;
    
    @Test
    @DisplayName("注文処理: 在庫あり・決済成功の場合、注文が確定される")
    void placeOrder_whenInStockAndPaymentSucceeds_confirmsOrder() {
        // Given(前提条件)
        OrderRequest request = new OrderRequest("PROD-001", 2, "CARD-123");
        
        given(inventoryService.checkStock("PROD-001", 2))
            .willReturn(true);
        given(paymentService.processPayment(eq("CARD-123"), anyDouble()))
            .willReturn(new PaymentResult(true, "TXN-001"));
        given(orderRepository.save(any(Order.class)))
            .willAnswer(invocation -> {
                Order order = invocation.getArgument(0);
                order.setId(1L);
                return order;
            });
        
        // When(実行)
        Order result = orderService.placeOrder(request);
        
        // Then(検証)
        then(inventoryService).should().checkStock("PROD-001", 2);
        then(paymentService).should().processPayment(eq("CARD-123"), anyDouble());
        then(orderRepository).should().save(any(Order.class));
        
        assertThat(result.getId()).isEqualTo(1L);
        assertThat(result.getStatus()).isEqualTo(OrderStatus.CONFIRMED);
    }
    
    @Test
    @DisplayName("注文処理: 在庫なしの場合、例外がスローされる")
    void placeOrder_whenOutOfStock_throwsException() {
        // Given
        OrderRequest request = new OrderRequest("PROD-001", 100, "CARD-123");
        given(inventoryService.checkStock("PROD-001", 100)).willReturn(false);
        
        // When & Then
        assertThatThrownBy(() -> orderService.placeOrder(request))
            .isInstanceOf(InsufficientStockException.class)
            .hasMessageContaining("PROD-001");
        
        then(paymentService).shouldHaveNoInteractions();
        then(orderRepository).shouldHaveNoInteractions();
    }
}

Java最新機能(Java 17-21+)

Java 17以降、LTS(Long-Term Support)リリースを中心に多くの重要な機能が追加された。ここでは、モダンJava開発で必須となる新機能を解説する。

Records(Java 16正式版)

Recordは、イミュータブルなデータキャリアクラスを簡潔に定義するための仕組みである。コンストラクタ、getter、equals()hashCode()toString()が自動生成される。

// 従来のデータクラス(数十行必要)
// Records を使うと1行で定義可能
public record Point(double x, double y) {}

// 使用例
Point p = new Point(3.0, 4.0);
System.out.println(p.x());       // 3.0(getterは x() 形式)
System.out.println(p.y());       // 4.0
System.out.println(p);           // Point[x=3.0, y=4.0]

// カスタムコンストラクタ(バリデーション付き)
public record UserRecord(
    String name,
    String email,
    int age
) {
    // コンパクトコンストラクタ(引数のthis.x = xは自動的に行われる)
    public UserRecord {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("名前は必須です");
        }
        if (age < 0 || age > 200) {
            throw new IllegalArgumentException("年齢は0-200の範囲です");
        }
        name = name.trim();  // 値の正規化
        email = email.toLowerCase();
    }
    
    // カスタムメソッドの追加
    public String displayName() {
        return "%s (%d歳)".formatted(name, age);
    }
}

// Recordは以下のインターフェースを実装可能
public sealed interface Shape permits Circle, Rectangle {
    double area();
}

public record Circle(double radius) implements Shape {
    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

public record Rectangle(double width, double height) implements Shape {
    @Override
    public double area() {
        return width * height;
    }
}

// RecordをDTOとして使用(Spring Bootとの統合)
public record ApiResponse<T>(
    int status,
    String message,
    T data,
    LocalDateTime timestamp
) {
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(200, "成功", data, LocalDateTime.now());
    }
    
    public static <T> ApiResponse<T> error(int status, String message) {
        return new ApiResponse<>(status, message, null, LocalDateTime.now());
    }
}

Sealed Classes(Java 17正式版)

Sealed Classesは、クラスやインターフェースの継承を制限し、許可されたサブクラスのみに継承を許す仕組みである。

// sealedキーワードで継承を制限
// permitsで許可するサブクラスを明示
public sealed interface PaymentMethod 
    permits CreditCard, BankTransfer, DigitalWallet {
    
    String getDisplayName();
    boolean validate();
}

// final: これ以上継承不可
public record CreditCard(
    String cardNumber, 
    String expiryDate, 
    String cvv
) implements PaymentMethod {
    @Override
    public String getDisplayName() { return "クレジットカード"; }
    
    @Override
    public boolean validate() {
        return cardNumber != null && cardNumber.length() == 16;
    }
}

// final: これ以上継承不可
public record BankTransfer(
    String bankCode, 
    String accountNumber
) implements PaymentMethod {
    @Override
    public String getDisplayName() { return "銀行振込"; }
    
    @Override
    public boolean validate() {
        return bankCode != null && accountNumber != null;
    }
}

// non-sealed: 自由に継承可能にする
public non-sealed class DigitalWallet implements PaymentMethod {
    private String walletId;
    
    @Override
    public String getDisplayName() { return "電子ウォレット"; }
    
    @Override
    public boolean validate() { return walletId != null; }
}

// Sealed Classesの利点: Pattern Matchingとの組み合わせで網羅性チェック可能

Pattern Matching

Pattern Matching for instanceof(Java 16正式版)

// 従来のinstanceof
Object obj = "Hello";
if (obj instanceof String) {
    String s = (String) obj;  // 明示的なキャストが必要
    System.out.println(s.length());
}

// Pattern Matching(Java 16+)
if (obj instanceof String s) {
    // sは既にString型として使用可能
    System.out.println(s.length());
}

// ガード条件付き
if (obj instanceof String s && s.length() > 3) {
    System.out.println("3文字以上の文字列: " + s);
}

Pattern Matching for switch(Java 21正式版)

// 型パターン
public String describe(Object obj) {
    return switch (obj) {
        case Integer i    -> "整数: %d".formatted(i);
        case Long l       -> "長整数: %d".formatted(l);
        case Double d     -> "小数: %.2f".formatted(d);
        case String s     -> "文字列: %s (長さ: %d)".formatted(s, s.length());
        case int[] arr    -> "配列: 長さ%d".formatted(arr.length);
        case null         -> "null";
        default           -> "不明な型: " + obj.getClass().getSimpleName();
    };
}

// ガード付きパターン(when句)
public String categorize(Object obj) {
    return switch (obj) {
        case Integer i when i < 0   -> "負の整数";
        case Integer i when i == 0  -> "ゼロ";
        case Integer i              -> "正の整数: " + i;
        case String s when s.isBlank() -> "空白文字列";
        case String s               -> "文字列: " + s;
        default                     -> "その他";
    };
}

// Sealed Classesとの組み合わせ(網羅性チェック)
public double calculateFee(PaymentMethod method) {
    return switch (method) {
        case CreditCard cc    -> cc.validate() ? 100.0 : 0.0;
        case BankTransfer bt  -> 50.0;
        case DigitalWallet dw -> 30.0;
        // Sealed Classなのでdefaultは不要(コンパイラが網羅性を検証)
    };
}

// Recordパターン(Java 21正式版)
public String formatPoint(Object obj) {
    return switch (obj) {
        case Point(var x, var y) when x == 0 && y == 0 -> "原点";
        case Point(var x, var y) -> "(%f, %f)".formatted(x, y);
        default -> "Pointではありません";
    };
}

// ネストしたRecordパターン
record Line(Point start, Point end) {}

public double length(Object obj) {
    return switch (obj) {
        case Line(Point(var x1, var y1), Point(var x2, var y2)) ->
            Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
        default -> 0.0;
    };
}

Text Blocks(Java 15正式版)

// JSON
String json = """
        {
            "name": "田中太郎",
            "age": 30,
            "address": {
                "city": "東京",
                "zip": "100-0001"
            }
        }
        """;

// SQL
String sql = """
        SELECT u.id, u.name, u.email,
               COUNT(o.id) AS order_count
        FROM users u
        LEFT JOIN orders o ON u.id = o.user_id
        WHERE u.status = 'ACTIVE'
          AND u.created_at >= ?
        GROUP BY u.id, u.name, u.email
        HAVING COUNT(o.id) > 0
        ORDER BY order_count DESC
        LIMIT 10
        """;

// HTML
String html = """
        <html>
            <body>
                <h1>%s</h1>
                <p>ようこそ、%sさん</p>
            </body>
        </html>
        """.formatted("タイトル", "田中");

// 末尾の空白を保持する場合は \s を使用
String preserveSpaces = """
        行1  \s
        行2  \s
        """;

// 改行を抑制する場合は \ を使用
String noNewline = """
        これは非常に長い文字列を\
        複数行に分けて記述する\
        ことができます""";
// → "これは非常に長い文字列を複数行に分けて記述することができます"

Virtual Threads(Java 21正式版)

// 従来のプラットフォームスレッド
Thread platformThread = Thread.ofPlatform()
    .name("platform-thread")
    .start(() -> System.out.println("Platform Thread"));

// Virtual Thread
Thread virtualThread = Thread.ofVirtual()
    .name("virtual-thread")
    .start(() -> System.out.println("Virtual Thread"));

// Virtual Threadベースのhttpサーバー的使い方
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i -> {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        });
    });
}

// Spring Boot 3.2+での有効化
// application.yml:
// spring.threads.virtual.enabled: true

Structured Concurrency(プレビュー / Java 21)

// Structured Concurrency: サブタスクのライフサイクルを親タスクに紐づける
/*
ScopedValue<UserContext> CONTEXT = ScopedValue.newInstance();

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Subtask<User> userTask = scope.fork(() -> findUser(userId));
    Subtask<List<Order>> ordersTask = scope.fork(() -> findOrders(userId));
    
    scope.join();           // すべてのサブタスクの完了を待機
    scope.throwIfFailed();  // いずれかが失敗した場合に例外をスロー
    
    // 両方のタスクが成功した場合のみここに到達
    return new UserProfile(userTask.get(), ordersTask.get());
}
*/

Scoped Values(プレビュー / Java 21)

// ThreadLocalの代替: イミュータブルで、Virtual Thread対応
/*
private static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();

// 値のバインド
ScopedValue.runWhere(CURRENT_USER, "admin", () -> {
    processRequest();  // この中でCURRENT_USER.get()で "admin" を取得可能
});

void processRequest() {
    String user = CURRENT_USER.get();  // "admin"
    System.out.println("処理中のユーザー: " + user);
}
*/

その他の便利な新機能

// String新メソッド
"  Hello  ".strip();          // "Hello"(Unicode対応のtrim)
"  Hello  ".stripLeading();   // "Hello  "
"  Hello  ".stripTrailing();  // "  Hello"
"".isBlank();                 // true
"Hello\nWorld".lines().toList(); // ["Hello", "World"]
"Ha".repeat(3);               // "HaHaHa"
" Hello ".indent(4);          // "     Hello \n"

// Stream新メソッド(Java 16+)
Stream.of(1, 2, 3).toList();  // 不変リストを返す

// mapMulti(Java 16+): flatMapの軽量版
List<Integer> result = List.of(1, 2, 3, 4, 5).stream()
    .<Integer>mapMulti((num, consumer) -> {
        if (num % 2 == 0) {
            consumer.accept(num);
            consumer.accept(num * 10);
        }
    })
    .toList();  // [2, 20, 4, 40]

// Sequenced Collections(Java 21)
SequencedCollection<String> list = new ArrayList<>(List.of("A", "B", "C"));
list.getFirst();    // "A"
list.getLast();      // "C"
list.addFirst("Z"); // 先頭に追加
list.reversed();    // 逆順ビュー

SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.put("one", 1);
map.put("two", 2);
map.firstEntry();  // one=1
map.lastEntry();   // two=2
map.reversed();    // 逆順ビュー

デプロイメントと運用

JARファイルとWARファイル

実行可能JAR(Fat JAR / Uber JAR)

Spring BootはデフォルトでFat JAR(依存関係をすべて含む単一JAR)を生成する。

# Mavenでビルド
mvn clean package -DskipTests

# Gradleでビルド
./gradlew bootJar

# 実行
java -jar target/demo-application-1.0.0.jar

# プロファイル指定で実行
java -jar demo-application.jar --spring.profiles.active=prod

# JVMオプション付きで実行
java -Xms512m -Xmx2g \
     -XX:+UseG1GC \
     -Dserver.port=8080 \
     -jar demo-application.jar

WAR(Web Application Archive)

アプリケーションサーバー(Tomcat、WildFlyなど)にデプロイする場合はWARを使用する。

// WAR用のSpring Boot設定
@SpringBootApplication
public class DemoApplication extends SpringBootServletInitializer {
    
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(DemoApplication.class);
    }
    
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
<!-- pom.xml: パッケージングをwarに変更 -->
<packaging>war</packaging>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope>
    </dependency>
</dependencies>

Dockerコンテナ化

Dockerfile(マルチステージビルド)

# ===== ビルドステージ =====
FROM eclipse-temurin:21-jdk AS builder

WORKDIR /app

# 依存関係のキャッシュ(ソースコード変更時に再ダウンロードを防ぐ)
COPY gradle/ gradle/
COPY gradlew build.gradle.kts settings.gradle.kts ./
RUN ./gradlew dependencies --no-daemon

# ソースコードのコピーとビルド
COPY src/ src/
RUN ./gradlew bootJar --no-daemon -x test

# JARの解凍(レイヤー最適化)
RUN java -Djarmode=layertools -jar build/libs/*.jar extract --destination extracted

# ===== 実行ステージ =====
FROM eclipse-temurin:21-jre

# セキュリティ: 非rootユーザーで実行
RUN groupadd -r appuser && useradd -r -g appuser -d /app appuser
WORKDIR /app

# Spring Boot レイヤーを活用(変更頻度の低い順にコピー)
COPY --from=builder /app/extracted/dependencies/ ./
COPY --from=builder /app/extracted/spring-boot-loader/ ./
COPY --from=builder /app/extracted/snapshot-dependencies/ ./
COPY --from=builder /app/extracted/application/ ./

# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD curl -f http://localhost:8080/actuator/health || exit 1

USER appuser

EXPOSE 8080

# JVMのコンテナ対応オプション
ENV JAVA_OPTS="-XX:+UseContainerSupport \
    -XX:MaxRAMPercentage=75.0 \
    -XX:+UseG1GC \
    -XX:+HeapDumpOnOutOfMemoryError \
    -XX:HeapDumpPath=/tmp/heapdump.hprof"

ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} org.springframework.boot.loader.launch.JarLauncher"]

Jibプラグイン(Dockerfileなしでイメージビルド)

// build.gradle.kts
plugins {
    id("com.google.cloud.tools.jib") version "3.4.0"
}

jib {
    from {
        image = "eclipse-temurin:21-jre"
        platforms {
            platform {
                architecture = "amd64"
                os = "linux"
            }
            platform {
                architecture = "arm64"
                os = "linux"
            }
        }
    }
    to {
        image = "registry.example.com/demo-app"
        tags = setOf("latest", project.version.toString())
    }
    container {
        jvmFlags = listOf(
            "-XX:+UseContainerSupport",
            "-XX:MaxRAMPercentage=75.0",
            "-XX:+UseG1GC"
        )
        ports = listOf("8080")
        environment = mapOf(
            "SPRING_PROFILES_ACTIVE" to "prod"
        )
        creationTime.set("USE_CURRENT_TIMESTAMP")
    }
}

docker-compose.yml

version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - DB_HOST=db
      - DB_PORT=3306
      - DB_NAME=mydb
      - DB_USERNAME=dbuser
      - DB_PASSWORD=dbpassword
    depends_on:
      db:
        condition: service_healthy
    networks:
      - app-network
    deploy:
      resources:
        limits:
          memory: 2G
          cpus: '2.0'

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: mydb
      MYSQL_USER: dbuser
      MYSQL_PASSWORD: dbpassword
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network

volumes:
  mysql-data:

networks:
  app-network:
    driver: bridge

GraalVM Native Image

GraalVMのNative Imageは、Javaアプリケーションをネイティブバイナリにコンパイルし、起動時間とメモリ消費を劇的に削減する。

# GraalVM Native Imageのビルド
./gradlew nativeCompile

# または Maven
mvn -Pnative native:compile

# 実行(通常のバイナリとして実行可能)
./build/native/nativeCompile/demo-application
// build.gradle.kts: GraalVM Native Image設定
plugins {
    id("org.graalvm.buildtools.native") version "0.9.28"
}

graalvmNative {
    binaries {
        named("main") {
            imageName.set("demo-app")
            mainClass.set("com.example.demo.DemoApplication")
            buildArgs.add("--enable-url-protocols=http,https")
            buildArgs.add("-H:+ReportExceptionStackTraces")
        }
    }
}

JMXによる監視

// カスタムMBeanの定義
public interface AppMetricsMXBean {
    long getRequestCount();
    double getAverageResponseTime();
    String getApplicationStatus();
}

@Component
public class AppMetrics implements AppMetricsMXBean {
    private final AtomicLong requestCount = new AtomicLong(0);
    private final AtomicReference<Double> avgResponseTime = new AtomicReference<>(0.0);
    
    @PostConstruct
    public void register() throws Exception {
        MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
        ObjectName name = new ObjectName("com.example:type=AppMetrics");
        mbs.registerMBean(this, name);
    }
    
    @Override
    public long getRequestCount() { return requestCount.get(); }
    
    @Override
    public double getAverageResponseTime() { return avgResponseTime.get(); }
    
    @Override
    public String getApplicationStatus() { return "RUNNING"; }
}
# JMXを有効にしてアプリケーションを起動
java -Dcom.sun.management.jmxremote \
     -Dcom.sun.management.jmxremote.port=9999 \
     -Dcom.sun.management.jmxremote.ssl=false \
     -Dcom.sun.management.jmxremote.authenticate=false \
     -jar demo-application.jar

JFR(Java Flight Recorder)

# JFRを有効にして起動
java -XX:StartFlightRecording=duration=60s,filename=recording.jfr \
     -jar demo-application.jar

# 実行中のアプリケーションにJFRを接続
jcmd <PID> JFR.start duration=120s filename=myrecording.jfr

# JFRの停止
jcmd <PID> JFR.stop name=1

# 記録の分析
jfr print --events jdk.GCPausePhase recording.jfr
jfr summary recording.jfr

ログ設定

SLF4J + Logback

<!-- logback-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    
    <!-- コンソール出力 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    
    <!-- JSON形式(本番環境向け) -->
    <springProfile name="prod">
        <appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>/var/log/app/application.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <fileNamePattern>/var/log/app/application.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
                <maxFileSize>100MB</maxFileSize>
                <maxHistory>30</maxHistory>
                <totalSizeCap>3GB</totalSizeCap>
            </rollingPolicy>
            <encoder class="net.logstash.logback.encoder.LogstashEncoder">
                <includeMdc>true</includeMdc>
                <includeContext>false</includeContext>
                <customFields>{"service":"demo-application","environment":"prod"}</customFields>
            </encoder>
        </appender>
    </springProfile>
    
    <!-- ログレベル設定 -->
    <logger name="com.example" level="DEBUG"/>
    <logger name="org.springframework" level="INFO"/>
    <logger name="org.hibernate.SQL" level="DEBUG"/>
    <logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
    
    <!-- 開発環境 -->
    <springProfile name="dev">
        <root level="INFO">
            <appender-ref ref="CONSOLE"/>
        </root>
    </springProfile>
    
    <!-- 本番環境 -->
    <springProfile name="prod">
        <root level="WARN">
            <appender-ref ref="JSON_FILE"/>
        </root>
    </springProfile>
</configuration>

ログの使用例

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

@Service
public class OrderService {
    private static final Logger log = LoggerFactory.getLogger(OrderService.class);
    
    public Order processOrder(OrderRequest request) {
        // MDC(Mapped Diagnostic Context)にリクエストIDを設定
        MDC.put("requestId", UUID.randomUUID().toString());
        MDC.put("userId", request.getUserId());
        
        try {
            log.info("注文処理開始: 商品={}, 数量={}", 
                request.getProductId(), request.getQuantity());
            
            // 処理ロジック
            Order order = createOrder(request);
            
            log.info("注文処理完了: 注文ID={}", order.getId());
            return order;
            
        } catch (Exception e) {
            log.error("注文処理エラー: 商品={}", request.getProductId(), e);
            throw e;
        } finally {
            MDC.clear();
        }
    }
}

まとめと参考資料

Javaの強みと課題

Javaの強み

  1. プラットフォーム非依存性: 「Write Once, Run Anywhere」の理念は、JVMの普及により確実に実現されている。同一のJARファイルがLinux、macOS、Windowsで動作する。

  2. エコシステムの成熟度: 30年近い歴史を持つJavaは、ライブラリ、フレームワーク、ツールのエコシステムが世界最大級である。ほぼあらゆる課題に対して、実績のあるソリューションが存在する。

  3. 後方互換性: Javaは後方互換性を非常に重視しており、古いバージョンで書かれたコードがそのまま新しいバージョンで動作することが保証されている。これは企業システムにとって極めて重要な特性である。

  4. パフォーマンス: JITコンパイラ、高度なGCアルゴリズム、JVMの最適化により、Javaは多くのベンチマークでC++に迫るパフォーマンスを実現している。特にサーバーサイドでの長時間実行において優位性がある。

  5. 堅牢な型システム: 静的型付け、ジェネリクス、Sealed Classes、Pattern Matchingなどにより、コンパイル時にバグを検出できる堅牢な型システムを提供する。

  6. 並行処理: Virtual Threads(Project Loom)の導入により、数百万スレッドの同時実行が可能になり、高スループットなサーバーアプリケーションの開発が格段に容易になった。

  7. エンタープライズでの信頼性: 世界の大企業の多くがJavaをメインの開発言語として採用しており、ミッションクリティカルなシステムでの実績が豊富である。金融、医療、官公庁など、高い信頼性が求められる分野で広く使われている。

  8. 活発なコミュニティ: OpenJDKを中心としたオープンソースコミュニティが活発に活動しており、6ヶ月ごとの定期リリースにより、最新の技術トレンドを取り入れ続けている。

Javaの課題

  1. 冗長性: Javaは他の現代的な言語(Kotlin、Python、Goなど)と比較して、記述量が多い傾向がある。Records、var、Text Blocksなどの新機能で改善されているが、完全には解消されていない。

  2. 起動時間とメモリ消費: JVMの起動には数百ミリ秒〜数秒を要し、メモリ消費も比較的大きい。GraalVM Native Imageで解決可能だが、リフレクションの制約がある。サーバーレスやFaaS環境ではコールドスタートが課題となる。

  3. ジェネリクスの型消去: 実行時に型パラメータ情報が失われる型消去の制約は、リフレクションや特定のパターンで問題を引き起こす。Project Valhallaで部分的に解消される見込みである。

  4. 関数型プログラミングの限界: ラムダ式やStream APIが導入されたが、パターンマッチング、代数的データ型、末尾呼び出し最適化などの面で、関数型言語に比べるとまだ限定的である。

  5. 学習コスト: フレームワーク(特にSpring Framework)の学習コストが高く、エコシステムの選択肢が多いため、初学者が迷いやすい。

今後の動向

Project Valhalla(値型)

プリミティブ型のように振る舞うユーザー定義型(Value Classes)を導入する。ヒープ割り当てとオブジェクトヘッダーのオーバーヘッドを排除し、配列のフラット化を実現する。ジェネリクスにプリミティブ型を使用できるようになる(List<int>)。

// 将来の構文(プレビュー段階)
value class Complex {
    double real;
    double imaginary;
}
// ヒープ割り当てなし、配列はフラット化

Project Panama(ネイティブ連携)

JNI(Java Native Interface)の代替として、Foreign Function & Memory API(FFM API)を提供する。ネイティブライブラリ(C/C++)との相互運用が安全かつ効率的になる。Java 22で正式版となった。

// Foreign Function & Memory API の例
// try (Arena arena = Arena.ofConfined()) {
//     MemorySegment segment = arena.allocate(100);
//     // ネイティブメモリの操作
// }

Project Amber(言語機能の改善)

Javaの言語機能を段階的に改善するプロジェクト。以下の機能が計画・開発中である。

  • Primitive types in Patterns: プリミティブ型のパターンマッチング
  • Statements before super(): コンストラクタのsuper()呼び出し前の文の許可
  • String Templates: 文字列補間(検討中)
  • Unnamed Variables: 未使用変数の_表記(Java 22正式版)
  • Implicitly Declared Classes: main メソッドの簡素化(Java 21プレビュー)
// Unnamed Variables(Java 22+)
try {
    // ...
} catch (Exception _) {  // 使わない変数は _ で宣言
    System.err.println("エラーが発生しました");
}

for (var _ : list) {  // 要素は不要、回数だけ繰り返す
    processNext();
}

// Implicitly Declared Classes(プレビュー)
// ファイル: HelloWorld.java
// クラス宣言なしで記述可能
void main() {
    System.out.println("Hello, World!");
}

Project Leyden(起動時間の短縮)

JVMの起動時間を短縮するための取り組み。クラスデータ共有(CDS)の拡張、AOT(Ahead-of-Time)コンパイルの改善、事前初期化などの技術を組み合わせて、JVMベースのアプリケーションの起動を高速化する。

参考文献

公式ドキュメント

書籍

  • Effective Java 第3版(Joshua Bloch著): Javaプログラミングのベストプラクティス集。すべてのJava開発者必読の書。
  • Java Concurrency in Practice(Brian Goetz著): Javaの並行処理を体系的に解説した名著。
  • Modern Java in Action(Raoul-Gabriel Urma他著): ラムダ式、Stream API、関数型プログラミングなどモダンJavaの機能を包括的に解説。
  • Spring in Action 第6版(Craig Walls著): Spring FrameworkとSpring Bootの包括的なガイド。
  • Clean Code(Robert C. Martin著): 言語に依存しない、良いコードの書き方の原則。

オンラインリソース

ツール


Javaは30年近い歴史を持ちながら、6ヶ月ごとの定期リリースにより常に進化を続けている。Virtual Threads、Pattern Matching、Recordsなどの新機能により、モダンな言語機能を取り入れつつ、後方互換性と堅牢性を維持している。エンタープライズ開発、クラウドネイティブアプリケーション、マイクロサービスなど、あらゆる規模のシステム開発において、Javaは今後も主要な選択肢であり続けるだろう。