Javaのラムダ式をやさしく理解する

Javaのラムダ式をやさしく理解する
Photo by Quilia / Unsplash

Javaのラムダ式は、1つのメソッドだけを持つインターフェース を短く書くための構文です。
とくに Stream APIComparatorRunnable、イベント処理などで頻繁に使われます。

この記事では、Javaのラムダ式について次の流れで整理します。

  1. ラムダ式とは何か
  2. 実務でよく使うパターン
  3. Runnable の意味と使いどころ
  4. compare(a.length(), b.length()) が昇順になる理由
  5. 身についたか確認するためのテスト

ラムダ式とは

まずは、無名クラスとの違いを見るとイメージしやすいです。

無名クラスで 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. sortComparator で並び替える

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 なら 0
  • x > y なら正の値

ソートでは一般的に、

  • 負の値なら「ab より前に置く」
  • 0 なら「同じ順序」
  • 正の値なら「ab より後ろに置く」

という意味になります。

つまり、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 は「どちらを先に並べるか決める」

とくに RunnableComparator が読めるようになると、ラムダ式への苦手意識はかなり減ります。
まずは forEachfiltermapsort の4パターンを手で書いて慣れるのがおすすめです。