Javaのラムダ式をやさしく理解する
Javaのラムダ式は、1つのメソッドだけを持つインターフェース を短く書くための構文です。
とくに Stream API、Comparator、Runnable、イベント処理などで頻繁に使われます。
この記事では、Javaのラムダ式について次の流れで整理します。
- ラムダ式とは何か
- 実務でよく使うパターン
Runnableの意味と使いどころcompare(a.length(), b.length())が昇順になる理由- 身についたか確認するためのテスト
ラムダ式とは
まずは、無名クラスとの違いを見るとイメージしやすいです。
無名クラスで Runnable を書くと次のようになります。
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
これをラムダ式で書くと、次のように短くできます。
Runnable r = () -> System.out.println("Hello");
構文の見方はシンプルです。
()は引数->は引数と処理の区切りSystem.out.println("Hello")は実行する処理
つまりラムダ式は、その場で渡したい小さな処理 を簡潔に表現するためのものです。
ラムダ式が使える条件
ラムダ式は、何にでも代入できるわけではありません。
使えるのは 関数型インターフェース に対してです。
関数型インターフェースとは、抽象メソッドを1つだけ持つインターフェース のことです。
たとえば Runnable は次のような形です。
public interface Runnable {
void run();
}
抽象メソッドが run() の1つだけなので、ラムダ式で書けます。
実務でよく使うパターン
ここからは、現場でよく見る書き方をパターンごとに見ていきます。
1. forEach で繰り返し処理
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = List.of("Tanaka", "Sato", "Suzuki");
names.forEach(name -> System.out.println(name));
}
}
forEach は、コレクションの各要素に対して順番に処理を実行します。
name -> System.out.println(name) は、「1件ずつ name として受け取って表示する」という意味です。
2. filter で条件に合うものだけ残す
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = List.of(10, 15, 20, 25, 30);
numbers.stream()
.filter(n -> n >= 20)
.forEach(n -> System.out.println(n));
}
}
filter は、条件に合う要素だけを残します。
n -> n >= 20 は、「n が20以上なら true」という条件です。
3. map でデータを変換する
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = List.of("tanaka", "sato", "suzuki");
names.stream()
.map(name -> name.toUpperCase())
.forEach(name -> System.out.println(name));
}
}
map は、各要素を別の形に変換するときに使います。
この例では、小文字の文字列を大文字へ変換しています。
4. sort と Comparator で並び替える
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("Takashi", "Ai", "Kenji"));
names.sort((a, b) -> Integer.compare(a.length(), b.length()));
System.out.println(names);
}
}
このコードは、文字列の長さが短い順 に並び替えています。
たとえば:
"Ai"の長さは2"Kenji"の長さは5"Takashi"の長さは7
そのため結果は次の順になります。
[Ai, Kenji, Takashi]
なぜ Integer.compare(a.length(), b.length()) で昇順になるのか
Integer.compare(x, y) は、次のルールで値を返します。
x < yなら負の値x == yなら0x > yなら正の値
ソートでは一般的に、
- 負の値なら「
aをbより前に置く」 0なら「同じ順序」- 正の値なら「
aをbより後ろに置く」
という意味になります。
つまり、Integer.compare(a.length(), b.length()) は次のように判定しています。
aの長さがbより短いなら負の値aの長さがbより長いなら正の値
その結果、短いものが前、長いものが後ろ になり、昇順になります。
逆に降順にしたいなら、引数の順番を逆にします。
names.sort((a, b) -> Integer.compare(b.length(), a.length()));
5. Runnable を使って処理を渡す
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("別スレッドで実行");
});
thread.start();
}
}
ここで出てくる Runnable は、実行したい処理そのもの を表すためのインターフェースです。
Runnable は抽象メソッドとして run() を1つだけ持っています。
public interface Runnable {
void run();
}
このため、ラムダ式で run() の中身をそのまま書けます。
Runnable r = () -> System.out.println("Start");
これは次の意味です。
- 引数はない
- 実行内容は
System.out.println("Start") - 必要になったら
run()される
new Thread(...) に Runnable を渡すと、その処理を別スレッドで動かせます。
つまり Runnable は、あとで実行する処理をオブジェクトとして渡すための箱 と考えると理解しやすいです。
6. 独自の関数型インターフェースを作る
@FunctionalInterface
interface Calculator {
int calc(int a, int b);
}
public class Main {
public static void main(String[] args) {
Calculator add = (a, b) -> a + b;
Calculator sub = (a, b) -> a - b;
System.out.println(add.calc(10, 5)); // 15
System.out.println(sub.calc(10, 5)); // 5
}
}
@FunctionalInterface は、「このインターフェースは関数型インターフェースとして使う」という意図を明確にするためのアノテーションです。
抽象メソッドを2つ以上定義してしまうとコンパイルエラーになるため、設計ミスにも気づきやすくなります。
標準でよく使う関数型インターフェース
Javaには、よくある用途向けの関数型インターフェースが最初から用意されています。
Predicate<T>
条件判定を表します。
import java.util.function.Predicate;
public class Main {
public static void main(String[] args) {
Predicate<Integer> isAdult = age -> age >= 18;
System.out.println(isAdult.test(20)); // true
System.out.println(isAdult.test(15)); // false
}
}
Function<T, R>
入力を別の値に変換します。
import java.util.function.Function;
public class Main {
public static void main(String[] args) {
Function<String, Integer> getLength = str -> str.length();
System.out.println(getLength.apply("Hello")); // 5
}
}
Consumer<T>
値を受け取って使うだけで、戻り値はありません。
import java.util.function.Consumer;
public class Main {
public static void main(String[] args) {
Consumer<String> printer = msg -> System.out.println(msg);
printer.accept("こんにちは");
}
}
Supplier<T>
引数なしで値を返します。
import java.util.function.Supplier;
public class Main {
public static void main(String[] args) {
Supplier<String> supplier = () -> "固定メッセージ";
System.out.println(supplier.get());
}
}
ラムダ式の書き方ルール
よく迷うポイントだけ押さえると、次のルールを覚えておけば十分です。
- 引数が1つなら
()を省略できる - 引数が2つ以上なら
()が必要 - 処理が1行なら
{}を省略できる returnを書く場合は{}が必要
たとえば次は省略形です。
x -> x * 2
次はブロック形式です。
x -> {
return x * 2;
}
よくある注意点
ラムダ式は便利ですが、何でも短く書けばよいわけではありません。
- ラムダ式が長くなりすぎると、かえって読みにくくなる
- 分岐や例外処理が増えるなら、通常のメソッドに切り出した方が読みやすい
- 「どの関数型インターフェースに渡しているのか」を意識すると理解しやすい
たとえば次のように長いラムダ式は読みにくくなりがちです。
list.forEach(x -> {
// 長い処理
// 分岐が多い
// 読みにくい
});
その場合は、メソッドへ分けた方がすっきりします。
list.forEach(x -> processUser(x));
まず覚えたい頻出パターン
最初は次の4つを自然に読めるようになると、かなり実務で役立ちます。
list.forEach(x -> System.out.println(x));
list.stream().filter(x -> x > 10).forEach(System.out::println);
list.stream().map(x -> x * 2).forEach(System.out::println);
list.sort((a, b) -> a.compareTo(b));
理解度テスト
ここからは、学んだ内容が身についたか確認するためのテストです。
答えを見ずに、まずは自力で考えてみてください。
問題1
次のラムダ式は何をしていますか。
s -> s.length()
問題2
次のコードの出力は何ですか。
List<Integer> nums = List.of(5, 12, 20);
nums.stream()
.filter(n -> n >= 10)
.forEach(n -> System.out.println(n));
問題3
空欄を埋めてください。
Function<String, Integer> func = ______;
System.out.println(func.apply("Java"));
期待する結果は 4 です。
問題4
次の無名クラスをラムダ式に書き換えてください。
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Start");
}
};
問題5
次のコードは何順に並び替えていますか。
names.sort((a, b) -> Integer.compare(a.length(), b.length()));
模範解答
自力で解いてから確認するのがおすすめです。
// 問題1
// 文字列 s を受け取り、その長さを返す
// 問題2
// 12
// 20
// 問題3
Function<String, Integer> func = str -> str.length();
// 問題4
Runnable r = () -> System.out.println("Start");
// 問題5
// 文字列の長さが短い順(昇順)
まとめ
Javaのラムダ式は、小さな処理を短く渡すための書き方 です。
最初は難しく見えても、実際には次の対応関係を押さえると理解しやすくなります。
forEachは「各要素に対して処理する」filterは「条件で絞る」mapは「変換する」Runnableは「あとで実行する処理を渡す」Comparatorは「どちらを先に並べるか決める」
とくに Runnable と Comparator が読めるようになると、ラムダ式への苦手意識はかなり減ります。
まずは forEach、filter、map、sort の4パターンを手で書いて慣れるのがおすすめです。