はじめに
株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。
Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
▶コーポレートサイト
「Java用語、結局なんだっけ?」シリーズの第5回です。
| 回 | テーマ |
|---|---|
| #1 | 環境・基盤編 |
| #2 | DB接続編 |
| #3 | Web/サーバー編 |
| #4 | 環境・DB・Web・例外スレッド |
| #5(本記事) | モダンJava編 |
| #6 | フレームワーク編 |
| #7 | ビルド・運用編 |
今回は モダンJava編 です。Java 8 以降の 関数型プログラミング寄りの機能 と、Java 16 で正式導入された record までを整理します。
この記事のゴール
- ラムダ式・メソッド参照・関数型インターフェースの関係を説明できる
- Stream API の基本パターンを使い分けられる
- Optional を正しく扱える
- record の用途を理解できる
① ラムダ式 ― 「メソッドを式として書く」
一言で言うと
「引数を受け取って何かを返す」処理を、短い式で書く構文 です。
Java 8 から導入。
旧スタイル vs ラムダ
// Java 7以前:匿名クラス
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("実行");
}
};
// Java 8以降:ラムダ式
Runnable task = () -> System.out.println("実行");
行数が 5行 → 1行 に。Runnable インターフェースが「メソッド1つだけ」だから可能になる仕組みです(後述)。
構文パターン
// 引数なし
() -> System.out.println("Hello")
// 引数1つ(型省略可)
n -> n * 2
// 引数複数
(a, b) -> a + b
// 複数行(中括弧と return が必要)
(a, b) -> {
int sum = a + b;
return sum * 2;
}
② 関数型インターフェース ― 「ラムダの受け皿」
一言で言うと
抽象メソッドが1つだけのインターフェース です。
ラムダ式は「関数型インターフェースの実装」として動きます。
標準で用意されているもの
| インターフェース | シグネチャ | 用途 |
|---|---|---|
Runnable |
void run() |
引数なし・戻り値なし |
Supplier<T> |
T get() |
引数なし・戻り値あり |
Consumer<T> |
void accept(T) |
引数あり・戻り値なし |
Function<T, R> |
R apply(T) |
引数あり・戻り値あり |
Predicate<T> |
boolean test(T) |
引数あり・真偽値を返す |
BiFunction<T, U, R> |
R apply(T, U) |
引数2つ |
使用例
Predicate<Integer> isEven = n -> n % 2 == 0;
System.out.println(isEven.test(4)); // → true
Function<String, Integer> length = s -> s.length();
System.out.println(length.apply("Hello")); // → 5
@FunctionalInterface アノテーション
自作で関数型インターフェースを作るときは、@FunctionalInterface を付けると安全です。
@FunctionalInterface
interface Calculator {
int calculate(int a, int b); // 抽象メソッドが2つ以上だとコンパイルエラー
}
Calculator add = (a, b) -> a + b;
System.out.println(add.calculate(3, 5)); // → 8
よくある誤解
- 「ラムダ式は新しい型の変数」:違う。関数型インターフェース型の実装。
- 「抽象メソッドが複数あっても関数型」:違う。1つだけでないと関数型インターフェースにならない。
③ メソッド参照 ― 「ラムダのさらに短い書き方」
一言で言うと
既存のメソッドをそのままラムダのように使える構文 です。
:: で書きます。
パターン
// 1. 静的メソッド参照
Function<String, Integer> parse = Integer::parseInt;
// 同じ意味のラムダ:s -> Integer.parseInt(s)
// 2. インスタンスメソッド参照(既存のオブジェクト)
PrintStream out = System.out;
Consumer<String> print = out::println;
// 同じ意味:s -> out.println(s)
// 3. インスタンスメソッド参照(型のメソッド)
Function<String, Integer> length = String::length;
// 同じ意味:s -> s.length()
// 4. コンストラクタ参照
Supplier<ArrayList<String>> creator = ArrayList::new;
// 同じ意味:() -> new ArrayList<>()
よくある誤解
-
「
::は型の指定」:違う。メソッドを参照する記号。 -
「
String::lengthは静的メソッド呼び出し」:違う。「文字列のlength()を呼ぶ関数」を作っている。
④ Stream API ― 「コレクション処理の宣言的記法」
一言で言うと
フィルタリング・変換・集計などを「処理の流れ」として書ける API です。
Java 8 から導入。
基本パターン
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> evenSquares = numbers.stream()
.filter(n -> n % 2 == 0) // 偶数だけ
.map(n -> n * n) // 二乗に変換
.collect(Collectors.toList());
System.out.println(evenSquares); // → [4, 16]
よく使うオペレーター
| メソッド | 用途 |
|---|---|
filter |
条件で絞り込み |
map |
要素を別の値に変換 |
flatMap |
ネストしたStreamをフラット化 |
sorted |
並び替え |
distinct |
重複除去 |
limit(n) |
最初のn個 |
count |
件数 |
sum、average
|
数値の集計(mapToInt 等の後) |
collect(toList()) |
List化 |
Stream API のメリット
- 意図がそのままコードに:「フィルタして」「変換して」「集めて」が読める
-
並列化が容易:
.parallelStream()で並列実行可能 - イミュータブル:元のリストを変更しない
よくある誤解
- 「Stream は遅い」:基本的に通常のforとほぼ同等。並列化(parallelStream)は逆に遅くなることもある。
- 「Stream は再利用できる」:違う。1回consumeしたら再利用不可。
⑤ Optional ― 「nullかもしれない値の安全な扱い」
一言で言うと
「値があるか、ないか」を型として表現するクラス です。
Java 8 から導入。
使い方
// nullになりうるメソッドの戻り値
public Optional<User> findUserById(long id) {
User user = repository.get(id);
return Optional.ofNullable(user);
}
// 呼び出し側
Optional<User> user = findUserById(1L);
// パターンA:デフォルト値
String name = user.map(User::getName).orElse("名無し");
// パターンB:例外を投げる
User u = user.orElseThrow(() -> new NotFoundException());
// パターンC:値があれば処理
user.ifPresent(u -> System.out.println(u.getName()));
「やってはいけない」使い方
// NG: Optional.get() の濫用
if (user.isPresent()) {
user.get().getName();
}
// → これは null チェックと変わらない。map / ifPresent を使う
// NG: フィールドに Optional
class User {
private Optional<String> email; // ← シリアライズ不可、設計ミスのサイン
}
// NG: メソッド引数に Optional
public void process(Optional<String> name) { } // ← オーバーロード推奨
Optional の使いどころ
- ✅ メソッドの戻り値(最も推奨される使い方)
- ❌ フィールドの型
- ❌ メソッドの引数
- ❌ コレクション内の要素
よくある誤解
-
「Optional はNPE対策の万能薬」:違う。
Optional自身が null になりうる(戻り値で null Optional は返すべきでない)。 -
「Optional.get() は安全」:違う。空のOptionalで呼ぶと
NoSuchElementException。NPEと変わらない。
⑥ record ― 「データクラスを1行で」
一言で言うと
フィールド宣言とコンストラクタ・getter・equals・hashCode・toString を自動生成するクラス です。
Java 14 でプレビュー、Java 16 で正式導入。
旧スタイル vs record
// 旧:通常クラス(30行近く)
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
@Override
public boolean equals(Object o) { /* 略 */ }
@Override
public int hashCode() { /* 略 */ }
@Override
public String toString() { /* 略 */ }
}
// 新:record(1行!)
public record Point(int x, int y) {}
record でできること
public record Point(int x, int y) {
// メソッド追加もできる
double distanceFromOrigin() {
return Math.sqrt(x * x + y * y);
}
// バリデーション付きコンストラクタ
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException("座標は0以上を指定");
}
}
}
// 使い方
Point p = new Point(3, 4);
System.out.println(p.x()); // → 3 (getter ではなく x() という形)
System.out.println(p.distanceFromOrigin()); // → 5.0
System.out.println(p); // → Point[x=3, y=4]
record の特徴
- すべてのフィールドが
final(不変クラス) - アクセサは
getX()ではなくx() -
equals・hashCode・toString自動生成 - 継承不可(
finalクラス)
record の使いどころ
- ✅ DTO(Data Transfer Object)
- ✅ 値オブジェクト(金額・座標・日付範囲等)
- ✅ API レスポンス・リクエストの型
- ❌ JPA エンティティ(可変が必要)
- ❌ 継承が必要なクラス
よくある誤解
-
「record はJavaの新しいキーワード」:違う。特殊なクラス宣言。実体は
Recordクラスのサブクラス。 -
「record で getter が
getX()になる」:違う。x()という形になる。
用語まとめ早見表
| 用語 | 一言で | 導入 |
|---|---|---|
| ラムダ式 | メソッドを式として書く構文 | Java 8 |
| 関数型インターフェース | 抽象メソッド1つだけのインターフェース | Java 8 |
| メソッド参照 | 既存メソッドを :: で参照 |
Java 8 |
| Stream API | コレクション処理の宣言的記法 | Java 8 |
| Optional | 値の有無を型で表現 | Java 8 |
| record | データクラスを1行で書く | Java 16 |
現場あるある誤解集
| ❌ 誤解 | ⭕ 正しい理解 |
|---|---|
| 「ラムダ = 新しい型の変数」 | 関数型インターフェースの実装 |
| 「Stream は配列より速い」 | 並列化しない限り、ほぼ同等 |
| 「Optional はNPE対策の万能薬」 | 使い方を誤るとNPEの代わりに別の例外が出る |
| 「Optional.get() が安全」 | 空だと例外。orElse / orElseThrow / map を使う |
| 「record は通常クラスの置き換え」 | 不変・継承不可なのでJPAエンティティ等には不向き |
| 「Optional をフィールドに持つ」 | 設計ミスのサイン。getterだけOptionalを返す |
| 「Stream を変数に保持して再利用」 | 1回consumeしたら再利用不可 |
演習問題
問題1:関数型インターフェース ⭐
次の関数型インターフェースのうち、String を引数に取り、int を返すものはどれですか?
- A.
Predicate<String> - B.
Consumer<String> - C.
Function<String, Integer> - D.
Supplier<Integer>
模範解答
正解:C
解説:
-
C
Function<String, Integer>→ 引数String、戻り値Integer(intにオートボクシング)。正解。 - A
Predicate<String>→ 引数String、戻り値boolean - B
Consumer<String>→ 引数String、戻り値void - D
Supplier<Integer>→ 引数なし、戻り値Integer
ポイント:Function<T, R> の T が引数の型、R が戻り値の型。
問題2:Stream API ⭐
次のコードを Stream API を使って書き直すとどうなりますか?
List<Integer> result = new ArrayList<>();
for (Integer n : numbers) {
if (n > 10) {
result.add(n * 2);
}
}
模範解答
List<Integer> result = numbers.stream()
.filter(n -> n > 10)
.map(n -> n * 2)
.collect(Collectors.toList());
ポイント:
- ループ+if+add の3ステップが、
filter+map+collectの3メソッドに対応 - 「10より大きい数を、2倍にして、リストに集める」がコード上に並ぶ
問題3:Optional ⭐
次の中で、Optional の正しい使い方 はどれですか?(複数選択可)
- A. メソッドの戻り値の型として使う
- B. クラスのフィールド型として使う
- C. メソッドの引数の型として使う
- D.
orElseでデフォルト値を指定する - E.
Optional.get()をif (opt.isPresent())の中で呼ぶ
模範解答
正解:A と D
解説:
- A:戻り値の型としてはOptionalの本来の使い方
- B:フィールドの型は通常型、getterで Optional を返す
- C:引数の型としては推奨されない(オーバーロードで対応)
-
D:
orElseは正しい使い方 -
E:「isPresent + get」は null チェックと変わらない。
map/ifPresent/orElse系を使う
ポイント:Optional は 戻り値の型として使うのが基本。フィールド・引数には使わない。
まとめ
モダンJavaの6用語のおさらいです。
- ラムダ式:メソッドを式として書く構文
- 関数型インターフェース:抽象メソッド1つのインターフェース(ラムダの受け皿)
-
メソッド参照:
::で既存メソッドを参照 - Stream API:コレクション処理の宣言的記法
- Optional:値の有無を型で表現
- record:データクラスを1行で書く
これらは現代Java開発のスタンダードです。
「使えるけど雰囲気でやっている」状態を脱して、設計意図まで言える ようになると、Spring Bootなどのフレームワークが格段に理解しやすくなります。
次回予告
次回(#6)は フレームワーク編 です。
- Spring / Spring Boot
- DI(依存性注入)、IoCコンテナ
- Bean、AOP
- Lombok、JPA
を解説します。
参考
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!