😽

【Java】ラムダ式っていつ使うんだ問題

に公開

背景

Javaを学んで大体の文法に慣れ始めたころ、急に現れる他とは毛色の違う謎の記法、ラムダ式。文法書やプロジェクトのコードで見かけて色々調べてみるも、難しくて理解できない…そんな存在ですよね!

それを踏まえて、この記事では、

  • ラムダ式はどんな時に使うのか?
  • ラムダ式の文法的な裏付け

上記をできるだけわかりやすく解説します。読めば最低限、ラムダ式の有用性と使い方がわかると思います。ラムダ式は使えるとコーディングの幅が大きく広がりますし、AIエージェントが頻繁に出力することも相まって非常に重要な記法です。ぜひこの記事でマスターして下さい!

※本記事ではわかりやすさのためラムダ式の詳細な部分は省いています(メソッド参照など)
本記事だけでも、手作業ではなくAIでコーディングする方なら十分な知識が身に付きますが、より詳細が気になる方は、もっと難しい記事を読んで理解を深めてみてください!
https://qiita.com/yoshitaro-yoyo/items/5a30915fc43a35ff10b4#3-5-ローカル変数の参照は-final-のみが可能

ラムダ式はどんな時に使うのか?

結論を言うと、「メソッドを引数として渡す」時に使います。これがどういったことなのか、例を使って解説します。

以下のような、

  • 名前
  • 得点
  • 性別

を持つ生徒(Student)クラスがあったとします。

public class Student {

    private String name;
    private int score;
    private Gender gender;

    public Student(String name, int score, Gender gender) {
        this.name = name;
        this.score = score;
        this.gender = gender;
    }

    public String getName() {
        return name;
    }

    public int getScore() {
        return score;
    }

    public Gender getGender() {
        return gender;
    }
}



複数人の生徒の内、「〇〇点以上」の生徒の平均得点を求めたかったら、以下のような実装になるでしょう。

public double averageScoreOfHighScorers(List<Student> students, int standardScore) {
    int sum = 0;
    int count = 0;

    for (Student s : students) {
        if (s.getScore() >= standardScore) {
            sum += s.getScore();
            count++;
        }
    }

    return count == 0 ? 0 : (double) sum / count;
}



では、「性別が〇」の生徒の平均得点を求めたかったらどうなるでしょうか?以下のようになるでしょう。

public double averageScoreByGender(List<Student> students, Gender gender) {
    int sum = 0;
    int count = 0;

    for (Student s : students) {
        if (s.getGender() == gender) {
            sum += s.getScore();
            count++;
        }
    }

    return count == 0 ? 0 : (double) sum / count;
}



さて、二つの平均点算出メソッドがありますが、この二つ、基本的な処理がほぼ同じです。違うのは生徒を絞り込む条件だけです。



やっていることが同じなのに二つのメソッドがあるのはソースコードの行数が増えて嬉しくありません。また、今後「名前があ行の生徒の平均点を求めたい」等のニーズが出てきたときに、新たにメソッドを作成する必要があり面倒です。

こんな場面で使えるのがラムダ式です。ラムダ式を使うと、メソッドの引数としてメソッドを渡すことができます。



メソッドが一本にまとまって嬉しいですね!コードに起こすと下記のようになります。

public class Main {

    private static double averageScore(
            List<Student> students,
            Predicate<Student> condition) {

        int sum = 0;
        int count = 0;

        for (Student s : students) {
            if (condition.test(s)) {
                sum += s.getScore();
                count++;
            }
        }

        return count == 0 ? 0 : (double) sum / count;
    }

    public static void main(String[] args) {

        List<Student> students = Arrays.asList(
                new Student("田中", 85, Gender.MALE),
                new Student("佐藤", 78, Gender.FEMALE),
                new Student("鈴木", 92, Gender.MALE),
                new Student("高橋", 60, Gender.MALE),
                new Student("山本", 88, Gender.FEMALE)
        );

        // ① 80点以上の生徒の平均
        double highScoreAverage = averageScore(
                students,
                s -> s.getScore() >= 80
        );

        // ② 男性生徒の平均
        double maleAverage = averageScore(
                students,
                s -> s.getGender() == Gender.MALE
        );

        System.out.println("80点以上の平均: " + highScoreAverage);
        System.out.println("男性の平均: " + maleAverage);
    }
}



上記の「s -> s.getScore() >= 80」と「s -> s.getGender() == Gender.MALE」がラムダ式の部分です。これはメソッドを表しています。sが引数で、returnがs.getScore() >= 80になる、という感じです。

ラムダ式の文法的な裏付け

さて、ラムダ式の便利さはわかっていただけたと思いますが、「なぜこんなことがまかり通るのか?」大いに疑問だと思います。

そんな皆さんの疑問を解きほぐしながら、なぜこのような記述が許されるのかを解説します。

なんで引数にメソッドを渡せるの?

Javaにおいて、引数に渡すことができるのはオブジェクト(インスタンス)とプリミティブな値(intとか)だけです。間違ってもメソッドなんて渡せません。では、前章で説明した例は何だったのか?

前章のメソッドは、実は「インターフェースを実装したクラスのインスタンス」だったのです!

実際、前章のコードのaverageScoreを見ると、引数としてPredicate型のインスタンスを指定していることがわかります。

private static double averageScore(
        List<Student> students,
        Predicate<Student> condition) {

    int sum = 0;
    int count = 0;

    for (Student s : students) {
        if (condition.test(s)) {
            sum += s.getScore();
            count++;
        }
    }

    return count == 0 ? 0 : (double) sum / count;
}



このPredicateはtestという抽象メソッドをもつインターフェースです。Predicateインターフェースの抽象メソッドtestを、「s -> s.getScore() >= 80」や「s -> s.getGender() == Gender.MALE」で実装したクラスを作り、そのインスタンスを引数として渡していた、というわけです。

そのことをわかりやすくするために、前章のコードをラムダ式を使わずに書いてみましょう。

① 80点以上かどうか判定するクラス

import java.util.function.Predicate;

public class HighScorePredicate implements Predicate<Student> {

    @Override
    public boolean test(Student s) {
        return s.getScore() >= 80;
    }
}



② 男性かどうか判定するクラス

import java.util.function.Predicate;

public class MaleStudentPredicate implements Predicate<Student> {

    @Override
    public boolean test(Student s) {
        return s.getGender() == Gender.MALE;
    }
}



mainメソッド側(クラスのインスタンスを渡す)

public class Main {

    private static double averageScore(
            List<Student> students,
            Predicate<Student> condition) {

        int sum = 0;
        int count = 0;

        for (Student s : students) {
            if (condition.test(s)) {
                sum += s.getScore();
                count++;
            }
        }

        return count == 0 ? 0 : (double) sum / count;
    }

    public static void main(String[] args) {

        List<Student> students = Arrays.asList(
                new Student("田中", 85, Gender.MALE),
                new Student("佐藤", 78, Gender.FEMALE),
                new Student("鈴木", 92, Gender.MALE),
                new Student("高橋", 60, Gender.MALE),
                new Student("山本", 88, Gender.FEMALE)
        );

        // クラスのインスタンスを生成
        Predicate<Student> highScorePredicate = new HighScorePredicate();
        Predicate<Student> malePredicate = new MaleStudentPredicate();

        // インスタンスを引数として渡す
        double highScoreAverage = averageScore(
                students,
                highScorePredicate
        );

        double maleAverage = averageScore(
                students,
                malePredicate
        );

        System.out.println("80点以上の平均: " + highScoreAverage);
        System.out.println("男性の平均: " + maleAverage);
    }
}



上記のコードは、ラムダ式を使って書いた前章のコードと完全に同じ意味です。しかし、同じ意味なのに明らかに前章より記述量が多いです。その上、可読性も悪いです。highScorePredicateやmalePredicateは一目見ただけではどのような処理が行われるのかわかりません。これがラムダ式の利点です。一々インターフェースを実装したクラスを作らなくても、「 引数 -> return 」だけでその処理を渡せるため、記述量が短く、そして読みやすいコードが書けるのです。

ラムダ式の手前、匿名クラス

ラムダ式が生まれるJava8より前はこう書いていました。

double highScoreAverage = averageScore(
        students,
        new Predicate<Student>() {
            @Override
            public boolean test(Student s) {
                return s.getScore() >= 80;
            }
        }
);

クラスの定義を「new 型()」の後に中括弧で書くというスタイルです。これで作られるクラスには名前が無いため、「匿名クラス」と呼ばれます。

この匿名クラスは、毎回クラス生成をする関係上メモリの負荷がかかります。そういった弱点を克服するために「関数を扱うための新しい仕組み」がJava8で作られました。それが「ラムダ式」というわけです。

まとめ

ラムダ式は「メソッドをメソッドの引数に渡す、簡潔で便利な記述法」です!皆さんもぜひ使ってみてください!

Discussion