Javaのラムダ式/StreamAPIについての初心者向けの例多めの説明

2024/03/05に公開

Javaのラムダ式は、Java 8以降でサポートされた強力な機能の一つです。
これはコードをより簡潔にし、特にコレクション[1]を操作する際に読みやすくするために導入されました。
この記事では、ラムダ式の基本的な使い方とそれを利用することの利点について説明します。

ラムダ式とは?

ラムダ式は簡単に言うと、匿名関数のようなものです。名前のない関数を簡単に記述できる方法です。Javaでは、これを関数型インターフェースを通じて実現しています。
関数型インターフェースとは、抽象メソッドを1つだけ持つインターフェースのことを指します。
作成したインターフェースに対し、 @FunctionalInterface アノテーションを付与すると関数的インターフェースであることを明示できます。

ラムダ式の例1

以下は、ラムダ式の書き方で加算と減算を行うシンプルな例です。

まず、関数型インターフェース Calculator を定義し、mainメソッドの中で addersubtractor の2つの計算方法を実装したインスタンスを生成します。

そして、Programクラスの printResult メソッドを以下のように定義します。

  1. 別途で定義された計算処理関数を受け取る
  2. printResult メソッドの中では具体的な処理内容を定義せず、引数で渡された処理内容に従って処理を行う[2]
@FunctionalInterface
public interface Calculator {
    // 二つの整数を受け取って計算する抽象メソッド
    int calculate(int x, int y);
}
public class Program {
    public static void main(String[] args) {
        // 加算を行う処理を実装した、Caluculateクラスのインスタンスを生成
        Calculator adder = (x, y) -> x + y;
        // 減算を行う処理を実装した、Caluculateクラスのインスタンスを生成
        Calculator subtractor = (x, y) -> x - y;
        
        printResult(10, 5, adder); // 15
        printResult(10, 5, subtractor); // 5
    }

    // メソッドの引数で関数を受け取る
    public static void printResult(int x, int y, Calculator calculator) {
        // 関数を呼び出して結果を表示する
        // 少し冗長だが、ラムダを受け取る側のメソッドからは
        // calculatorオブジェクトのcalculateメソッドを呼ぶという形になってしまう
        int result = calculator.calculate(x, y);

        System.out.println(result); // xとyをcaluculator.calculateメソッドで計算した結果
    }
}

この例では、 Calculator という関数型インターフェースを定義し、それを実装する形で加算と減算のラムダ式を作成しています。
そして、 printResult メソッドの中で、これらのラムダ式を利用しています。

ラムダ式の例2

次は少し利用シーンに近い例にします。
ArrayList に格納された、Employee クラスの従業員オブジェクトから、特定の条件に一致するオブジェクトだけをフィルタリングする例です。
この例では、年齢が30歳以上の従業員だけをリストから選択しています。

class Employee {
    private String name;
    private int age;

    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class EmployeeFilterExample {
    public static void main(String[] args) {
        // 従業員のリストを作成
        List<Employee> employees = new ArrayList<>();
        employees.add(new Employee("齊藤京子", 26));
        employees.add(new Employee("秋元真夏", 30));
        employees.add(new Employee("国生さゆり", 57));

        // 年齢が30歳以上の従業員だけをフィルタリング
        List<Employee> filteredEmployees = employees.stream()
                .filter(employee -> employee.getAge() >= 30)
                .collect(Collectors.toList());

        // 結果を表示
        filteredEmployees.forEach(employee -> 
            System.out.println(employee.getName() + ", " + employee.getAge()));
        // 秋元真夏, 30
        // 国生さゆり, 57
    }
}

ここで、 filter() メソッドに渡しているラムダ式はこちらPredicate の関数型インターフェースです。
引数に渡されたものが条件に合致するかどうかを判定してtrueかfalseを返す関数を渡すことで、リストの要素1つ1つについて指定した条件式に合致するものだけを集めて新しいリストに集めるといった動きになります。

ちなみに、同じ動作をするものをfor-eachループで書き直すと以下のようになります。

public class EmployeeFilterExample {
    public static void main(String[] args) {
        // 従業員のリストを作成
        List<Employee> employees = new ArrayList<>();
        employees.add(new Employee("齊藤京子", 26));
        employees.add(new Employee("秋元真夏", 30));
        employees.add(new Employee("国生さゆり", 57));

        // 年齢が30歳以上の従業員だけをフィルタリングして表示
        for (Employee employee : employees) {
            if (employee.getAge() >= 30) {
                System.out.println(employee.getName() + ", " + employee.getAge());
            }
        }
    }
}

ラムダ式の例3

ラムダ式/StreamAPIの可読性を際立たせるため、もっと複雑なものにします。
全国の営業所の中で九州地方の営業所から売上で上位3箇所を抽出し、それが九州地方の他の営業所の中で何パーセントを占めるのかを算出するサンプルです。

// 営業所を表すクラス
class SalesOffice {
    private String name;
    private String region;
    private double sales;

    public SalesOffice(String name, String region, double sales) {
        this.name = name;
        this.region = region;
        this.sales = sales;
    }

    public String getName() {
        return name;
    }

    public String getRegion() {
        return region;
    }

    public double getSales() {
        return sales;
    }
}
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class SalesOfficeAnalysis {
    public static void main(String[] args) {
        // 全国の営業所リスト
        List<SalesOffice> offices = new ArrayList<>();
        // 例として、九州地方とその他の地方の営業所を追加
        offices.add(new SalesOffice("福岡営業所", "九州", 1000));
        offices.add(new SalesOffice("長崎営業所", "九州", 800));
        offices.add(new SalesOffice("熊本営業所", "九州", 1200));
        offices.add(new SalesOffice("大分営業所", "九州", 700));
        offices.add(new SalesOffice("宮崎営業所", "九州", 900));
        offices.add(new SalesOffice("鹿児島営業所", "九州", 1100));
        offices.add(new SalesOffice("沖縄営業所", "九州", 500));
        // 九州以外の営業所も追加
        offices.add(new SalesOffice("東京営業所", "関東", 2000));
        offices.add(new SalesOffice("大阪営業所", "関西", 1500));

        // 九州地方の営業所のみをフィルタリング
        List<SalesOffice> kyushuOffices = offices.stream()
                .filter(office -> office.getRegion().equals("九州"))
                .collect(Collectors.toList());

        // 九州地方の営業所の売上合計
        double totalSalesInKyushu = kyushuOffices.stream()
                .mapToDouble(office -> office.getSales()) // 営業所オブジェクトから売上を取得。営業所クラスのストリームを売上(double型)のストリームに変換している
                .sum(); // 売上の合計。数値のStreamに対してはこれだけでsumが求められる

        // 九州地方の営業所の中で売上上位3箇所を抽出
        List<SalesOffice> top3SalesOfficesInKyushu = kyushuOffices.stream()
                .sorted(Comparator.comparingDouble(SalesOffice::getSales).reversed()) // 売上の降順→売上の高い順にソート
                .limit(3) // 先頭から3要素だけにする、つまり売上の高い順に3営業所を抽出している
                .collect(Collectors.toList());

        // 上位3箇所の売上合計
        double top3SalesTotal = top3SalesOfficesInKyushu.stream()
                .mapToDouble(office -> office.getSales())
                .sum();

        // 上位3箇所が九州地方の売上合計に占める割合を算出
        double percentage = (top3SalesTotal / totalSalesInKyushu) * 100;

        System.out.println("九州地方の上位3営業所の売上合計は九州地方全体の売上の " + String.format("%.2f", percentage) + "% を占めます。");
        // 九州地方の上位3営業所の売上合計は九州地方全体の売上の 53.23% を占めます。
    }
}

これをforループで書き直すとこうなります。

public class SalesOfficeAnalysis {
    public static void main(String[] args) {
        // 全国の営業所リスト
        List<SalesOffice> offices = new ArrayList<>();
        offices.add(new SalesOffice("福岡営業所", "九州", 1000));
        offices.add(new SalesOffice("長崎営業所", "九州", 800));
        offices.add(new SalesOffice("熊本営業所", "九州", 1200));
        offices.add(new SalesOffice("大分営業所", "九州", 700));
        offices.add(new SalesOffice("宮崎営業所", "九州", 900));
        offices.add(new SalesOffice("鹿児島営業所", "九州", 1100));
        offices.add(new SalesOffice("沖縄営業所", "九州", 500));
        offices.add(new SalesOffice("東京営業所", "関東", 2000));
        offices.add(new SalesOffice("大阪営業所", "関西", 1500));

        // 九州地方の営業所のみをフィルタリング
        List<SalesOffice> kyushuOffices = new ArrayList<>();
        for (SalesOffice office : offices) {
            if ("九州".equals(office.getRegion())) {
                kyushuOffices.add(office);
            }
        }

        // 九州地方の営業所の売上合計
        double totalSalesInKyushu = 0;
        for (SalesOffice office : kyushuOffices) {
            totalSalesInKyushu += office.getSales();
        }

        // 九州地方の営業所の中で今月の売上上位3箇所を抽出
        kyushuOffices.sort((o1, o2) -> Double.compare(o2.getSales(), o1.getSales()));
        List<SalesOffice> top3SalesOfficesInKyushu = new ArrayList<>();
        for (int i = 0; i < Math.min(3, kyushuOffices.size()); i++) {
            top3SalesOfficesInKyushu.add(kyushuOffices.get(i));
        }

        // 上位3箇所の売上合計
        double top3SalesTotal = 0;
        for (SalesOffice office : top3SalesOfficesInKyushu) {
            top3SalesTotal += office.getSales();
        }

        // 上位3箇所が九州地方の売上合計に占める割合を算出
        double percentage = (top3SalesTotal / totalSalesInKyushu) * 100;

        System.out.println("九州地方の上位3営業所の売上合計は九州地方全体の売上の " + String.format("%.2f", percentage) + "% を占めます。");
        // 九州地方の上位3営業所の売上合計は九州地方全体の売上の 53.23% を占めます。
    }
}

forループに縛る場合、定義したい処理を直接的/宣言的に表しにくくなっていると思います。
特に、

     // 九州地方の営業所の中で売上上位3箇所を抽出
        List<SalesOffice> top3SalesOfficesInKyushu = kyushuOffices.stream()
                .sorted(Comparator.comparingDouble(SalesOffice::getSales).reversed()) // 売上の降順→売上の高い順にソート
                .limit(3) // 先頭から3要素だけにする、つまり売上の高い順に3営業所
                .collect(Collectors.toList());

と直接的に表現できるところが、

     // 九州地方の営業所の中で今月の売上上位3箇所を抽出
        kyushuOffices.sort((o1, o2) -> Double.compare(o2.getSales(), o1.getSales()));
        List<SalesOffice> top3SalesOfficesInKyushu = new ArrayList<>();
        for (int i = 0; i < Math.min(3, kyushuOffices.size()); i++) {
            top3SalesOfficesInKyushu.add(kyushuOffices.get(i));
        }

と脳内で動作をエミュレーションするか、コメントを残すか慣れてないと難読なコードとなってしまいます。

ちなみに以下のコードは、昇順ソートするキーを抽出するためのメソッドはSalesOfficeのgetSalesですよ、といった意味です。関数渡しの書き方です。

Comparator.comparingDouble(SalesOffice::getSales)

ラムダ式の利点

上記のような例でもラムダ式/StreamAPIを利用する利点を見いだせると思います。簡潔性が得られ、ラムダ式を使用することで、コードがより簡潔になり、読みやすくなります。
また、ここでは触れていないですが並列処理の実装も可能で、ラムダ式はJava 8で導入されたStream APIと組み合わせることで、コレクションの並列処理を簡単に行うことができ、ケースによっては処理時間の短縮等と簡潔な書き方の両立ができます。

脚注
  1. リスト、セット、マップなど複数の要素を配列のように格納できるオブジェクト。JavaではArrayList、HashSet、HashMapなどがこれにあたります ↩︎

  2. こういった関数を渡す方法、関数渡しは例えば身近なものだとフロントエンドでボタンクリック時の処理内容をボタンクリックイベントに紐付けるといった場面で多用されます ↩︎

Discussion