はじめに
Javaのコードを読んでいて、こんな記法に出会ったことはありませんか?
() -> new RuntimeException("エラー")
name -> name.length()
(a, b) -> a + b
これがラムダ式です。Java 8で導入された機能で、「処理そのもの」を値として扱える記法です。
この記事では「ラムダ式って何?」というレベルから、実務で使えるところまで段階的に解説します。
ラムダ式の前に:なぜ必要なのか
例:リストの並び替え
ラムダ式がなかった時代(Java 7以前)は、ちょっとした処理を渡すだけでもこう書く必要がありました。
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class BeforeLambda {
public static void main(String[] args) {
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
// 匿名クラスで並び替えの基準を渡す
names.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
System.out.println(names); // [Alice, Bob, Charlie]
}
}
やりたいことは「a.compareTo(b)で比較する」だけなのに、6行もかかっています。
ラムダ式なら1行
import java.util.Arrays;
import java.util.List;
public class WithLambda {
public static void main(String[] args) {
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
names.sort((a, b) -> a.compareTo(b));
System.out.println(names); // [Alice, Bob, Charlie]
}
}
(a, b) -> a.compareTo(b) — これがラムダ式です。
ラムダ式の基本構文
(引数) -> 処理
| 要素 | 説明 |
|---|---|
() |
引数(なければ空カッコ) |
-> |
アロー演算子(「~したら」と読むとわかりやすい) |
| 右辺 | 実行する処理 |
パターン別の書き方
// 引数なし
() -> System.out.println("Hello")
// 引数1つ(カッコ省略可)
name -> name.length()
// 引数2つ
(a, b) -> a + b
// 処理が複数行(中括弧 + return が必要)
(a, b) -> {
int sum = a + b;
return sum;
}
関数型インターフェースとは
ラムダ式は関数型インターフェースの実装として使います。
関数型インターフェースとは、抽象メソッドが1つだけのインターフェースのことです。
public class FunctionalInterfaceDemo {
// これが関数型インターフェース(抽象メソッドが1つだけ)
@FunctionalInterface
interface Greeting {
String say(String name);
}
public static void main(String[] args) {
// 匿名クラスで書く場合
Greeting greeting1 = new Greeting() {
@Override
public String say(String name) {
return "こんにちは、" + name + "さん!";
}
};
// ラムダ式で書く場合(同じ意味)
Greeting greeting2 = name -> "こんにちは、" + name + "さん!";
System.out.println(greeting1.say("太郎")); // こんにちは、太郎さん!
System.out.println(greeting2.say("花子")); // こんにちは、花子さん!
}
}
ラムダ式は、匿名クラスの省略記法と考えるとわかりやすいです。
なぜ型を書かなくていいのか
// Greeting の say メソッドは String を受け取って String を返す
// → Javaが自動で型を推論してくれる
Greeting greeting = name -> "こんにちは、" + name + "さん!";
// ^^^^
// String型と推論される
Java標準の関数型インターフェース
自分で定義しなくても、Javaには標準の関数型インターフェースが用意されています。
| インターフェース | メソッド | 用途 |
|---|---|---|
Supplier<T> |
T get() |
引数なしで値を返す |
Consumer<T> |
void accept(T t) |
値を受け取って処理する(戻り値なし) |
Function<T, R> |
R apply(T t) |
値を受け取って変換して返す |
Predicate<T> |
boolean test(T t) |
値を受け取って判定する |
Runnable |
void run() |
引数なし・戻り値なし |
Supplier:値を供給する
import java.util.function.Supplier;
public class SupplierDemo {
public static void main(String[] args) {
// 「呼ばれたら "Hello" を返す」処理
Supplier<String> supplier = () -> "Hello";
System.out.println(supplier.get()); // Hello
}
}
orElseThrowの引数はまさにこのSupplierです。
// Supplier<RuntimeException> として渡している
.orElseThrow(() -> new RuntimeException("not found"))
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// 引数なし → 例外を返す = Supplier
Consumer:値を消費する
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class ConsumerDemo {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// 各要素に対して処理を実行
Consumer<String> printer = name -> System.out.println("名前: " + name);
names.forEach(printer);
// 名前: Alice
// 名前: Bob
// 名前: Charlie
// 直接ラムダ式を渡すことが多い
names.forEach(name -> System.out.println("Hello, " + name));
}
}
Function:値を変換する
import java.util.function.Function;
public class FunctionDemo {
public static void main(String[] args) {
// String を受け取って、その長さ(Integer)を返す
Function<String, Integer> lengthFunc = str -> str.length();
System.out.println(lengthFunc.apply("Hello")); // 5
System.out.println(lengthFunc.apply("Java")); // 4
}
}
Predicate:条件判定する
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PredicateDemo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 偶数かどうか判定
Predicate<Integer> isEven = n -> n % 2 == 0;
List<Integer> evenNumbers = numbers.stream()
.filter(isEven)
.collect(Collectors.toList());
System.out.println(evenNumbers); // [2, 4, 6, 8, 10]
}
}
実践:Spring Bootでよく見るラムダ式
1. orElseThrow(Supplier)
public Todo findById(Long id) {
return todoRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Todo not found: id " + id));
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Supplier<RuntimeException>
}
2. forEach(Consumer)
List<Todo> todos = todoRepository.findAll();
todos.forEach(todo -> System.out.println(todo.getTitle()));
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Consumer<Todo>
3. Stream API(Predicate / Function)
List<String> completedTitles = todoRepository.findAll().stream()
.filter(todo -> todo.isCompleted()) // Predicate<Todo>
.map(todo -> todo.getTitle()) // Function<Todo, String>
.collect(Collectors.toList());
メソッド参照(おまけ)
ラムダ式をさらに短く書ける記法です。
// ラムダ式
names.forEach(name -> System.out.println(name));
// メソッド参照(同じ意味)
names.forEach(System.out::println);
// ラムダ式
.map(todo -> todo.getTitle())
// メソッド参照(同じ意味)
.map(Todo::getTitle)
「引数をそのまま1つのメソッドに渡すだけ」の場合に使えます。
まとめ
| 概念 | 意味 |
|---|---|
| ラムダ式 | 「処理そのもの」を値として渡せる記法 |
() -> |
引数なしのラムダ式 |
x -> |
引数1つのラムダ式 |
(a, b) -> |
引数2つのラムダ式 |
| 関数型インターフェース | 抽象メソッドが1つだけのインターフェース |
Supplier<T> |
引数なし → 値を返す |
Consumer<T> |
値を受け取る → 戻り値なし |
Function<T, R> |
値を受け取る → 変換して返す |
Predicate<T> |
値を受け取る → true/false を返す |
ラムダ式は概念的には匿名クラスの省略記法と考えるとわかりやすいです。関数型インターフェースの「唯一の抽象メソッド」の実装をインラインで書いている、と理解すればOKです。
※ 厳密にはラムダ式と匿名クラスは別の仕組みです(thisの参照先が異なるなど)。
演習問題
問題1 ⭐(基本)
以下の匿名クラスをラムダ式に書き換えてください。
import java.util.function.Function;
public class Exercise1 {
public static void main(String[] args) {
Function<String, Integer> toLength = new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return s.length();
}
};
System.out.println(toLength.apply("Java")); // 4
}
}
模範解答
import java.util.function.Function;
public class Exercise1 {
public static void main(String[] args) {
Function<String, Integer> toLength = s -> s.length();
System.out.println(toLength.apply("Java")); // 4
}
}
Function<String, Integer>のメソッドはInteger apply(String s)なので、Stringを受け取ってIntegerを返すラムダ式を書けばOKです。
問題2 ⭐⭐(応用)
Predicateとstreamを使って、文字列リストから5文字以上の単語だけを抽出してください。
import java.util.Arrays;
import java.util.List;
public class Exercise2 {
public static void main(String[] args) {
List<String> words = Arrays.asList("cat", "elephant", "dog", "giraffe", "ant", "tiger");
// ここにコードを書く
// 期待する出力: [elephant, giraffe, tiger]
}
}
模範解答
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Exercise2 {
public static void main(String[] args) {
List<String> words = Arrays.asList("cat", "elephant", "dog", "giraffe", "ant", "tiger");
List<String> longWords = words.stream()
.filter(word -> word.length() >= 5)
.collect(Collectors.toList());
System.out.println(longWords); // [elephant, giraffe, tiger]
}
}
filterの引数はPredicate<String>です。word -> word.length() >= 5は「文字列を受け取ってbooleanを返す」ラムダ式なので、Predicateの型に合致します。
問題3 ⭐⭐⭐(チャレンジ)
以下のUserServiceクラスのfindByEmailメソッドを、OptionalとSupplier(ラムダ式)を使って書き換えてください。nullチェックのif文を使わないこと。
import java.util.HashMap;
import java.util.Map;
public class UserService {
private Map<String, String> users = new HashMap<>(Map.of(
"taro@example.com", "太郎",
"hanako@example.com", "花子"
));
public String findByEmail(String email) {
String name = users.get(email);
if (name != null) {
return name;
} else {
throw new RuntimeException("User not found: " + email);
}
}
public static void main(String[] args) {
UserService service = new UserService();
System.out.println(service.findByEmail("taro@example.com")); // 太郎
System.out.println(service.findByEmail("nobody@example.com")); // RuntimeException
}
}
模範解答
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class UserService {
private Map<String, String> users = new HashMap<>(Map.of(
"taro@example.com", "太郎",
"hanako@example.com", "花子"
));
public String findByEmail(String email) {
return Optional.ofNullable(users.get(email))
.orElseThrow(() -> new RuntimeException("User not found: " + email));
}
public static void main(String[] args) {
UserService service = new UserService();
System.out.println(service.findByEmail("taro@example.com")); // 太郎
System.out.println(service.findByEmail("nobody@example.com")); // RuntimeException
}
}
-
Optional.ofNullable():nullかもしれない値をOptionalで包む -
orElseThrow(Supplier):値があればそれを返し、なければSupplierが生成する例外をスロー -
() -> new RuntimeException(...):Supplier<RuntimeException>のラムダ式
参考
- https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html
- https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
- https://docs.oracle.com/javase/specs/jls/se21/html/jls-15.html#jls-15.27
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!