CodeGym /コース /JAVA 25 SELF /関数をパラメータとして渡す: 例

関数をパラメータとして渡す: 例

JAVA 25 SELF
レベル 49, レッスン 2
使用可能

1. データの代わりに振る舞いを渡す

なぜ関数をパラメータとして渡すのか?

想像してみてください。あなたはリストをソートするメソッドを持っています。しかし、どのようにソートすべきかをメソッドはどうやって知るのでしょう? アルファベット順? 長さ順? 誕生日順? ケースごとに別々のメソッドを書くこともできますが、それではすぐにコピペ地獄になります。

そこで Java では、ソート・フィルタリング・変換などのやり方を決める振る舞い(関数やラムダ)を渡せます。これにより、コードは柔軟で再利用しやすくなります。

例: Comparator を使ったソート

List<String> names = List.of("Anna", "Boris", "Vika");

// 振る舞いを渡す: 要素の比較方法(文字列の長さ)
names.stream()
     .sorted((a, b) -> a.length() - b.length())
     .forEach(System.out::println);

例: Predicate を使ったフィルタリング

List<String> names = List.of("Anna", "Boris", "Vika");

// 振る舞いを渡す: 残す条件(名前が 4 文字より長い)
names.stream()
     .filter(name -> name.length() > 4)
     .forEach(System.out::println);

どちらの場合も、ロジックをメソッドに「埋め込む」のではなく、メソッドに小さな振る舞いを渡し、それを各要素に適用させています。

利点: 重複の削減と柔軟性の向上

似たコードだがロジックだけが違うメソッドを大量に作る代わりに、「何をするか」を関数として受け取る汎用メソッドを 1 つ書くだけで済みます。時間を節約し、ミスを減らし、テストもしやすくなります。

2. 関数を渡すための構文

引数としてのラムダ式

最も一般的なのは、メソッド呼び出しのその場で -> を使ってラムダを書く方法です:

list.forEach(item -> System.out.println(item));

あるいは、ちょうどよいメソッドがすでにあるなら、さらに短くメソッド参照(::)を使えます:

list.forEach(System.out::println);

メソッド参照 (method references)

既存の適切なメソッドがあれば、:: で参照を渡せます:

// 通常のメソッド
public static boolean isLongName(String name) {
    return name.length() > 4;
}

// メソッド参照の渡し方
names.stream()
     .filter(MyClass::isLongName)
     .forEach(System.out::println);

これは、そのメソッドのシグネチャが、期待される関数型インターフェース(Predicate<T>、Function<T, R>、Comparator<T> など)に一致している場合に機能します。

3. 標準ライブラリからの例

Collections.sort と Comparator

List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));

// 名前の長さでソート
names.sort((a, b) -> a.length() - b.length());
System.out.println(names); // [Vika, Anna, Boris]

Stream.filter と Predicate

List<String> names = List.of("Anna", "Boris", "Vika");

// 'V' で始まる名前だけ残す
names.stream()
     .filter(name -> name.startsWith("V"))
     .forEach(System.out::println); // Vika

Stream.map と Function

List<String> names = List.of("Anna", "Boris", "Vika");

// 名前を大文字に変換
names.stream()
     .map(String::toUpperCase)
     .forEach(System.out::println);

Optional.ifPresent と Consumer

Optional<String> opt = Optional.of("こんにちは!");

// 値が存在する場合は出力する
opt.ifPresent(s -> System.out.println("文字列: " + s));

4. 実践: 関数をパラメータに取る自作メソッド

そろそろ実践しましょう。関数をパラメータとして受け取り、内部で使うメソッドをいくつか書いてみます。

例 1: リスト要素の処理メソッド(独自の forEach)

ユーザー(User)のリストがあるとします。各ユーザーに対して何らかの処理をしたい — たとえば、名前を表示する、メールを送る、ボーナスを付与する、など。処理をメソッドに「埋め込む」のではなく、Consumer<User> としてパラメータで渡しましょう。

import java.util.List;
import java.util.function.Consumer;

class User {
    String name;
    int age;
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class UserProcessor {
    // Consumer<User> を受け取るメソッド
    public static void processUsers(List<User> users, Consumer<User> action) {
        for (User user : users) {
            action.accept(user);
        }
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 30),
            new User("Vika", 22)
        );

        // すべてのユーザーの名前を表示
        processUsers(users, user -> System.out.println("名前: " + user.name));

        // ボーナスを付与(ここでは出力だけ)
        processUsers(users, user -> System.out.println(user.name + " はボーナスを受け取りました!"));
    }
}

例 2: 要素を抽出するフィルタメソッド(Predicate)

条件に合うユーザーだけを返すメソッドを書きます(例: 成人のみ)。

import java.util.List;
import java.util.ArrayList;
import java.util.function.Predicate;

public class UserFilter {
    public static List<User> filterUsers(List<User> users, Predicate<User> condition) {
        List<User> result = new ArrayList<>();
        for (User user : users) {
            if (condition.test(user)) {
                result.add(user);
            }
        }
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 17),
            new User("Vika", 22)
        );

        // 成人のみを抽出
        List<User> adults = filterUsers(users, user -> user.age >= 18);
        adults.forEach(user -> System.out.println(user.name)); // Anna, Vika
    }
}

例 3: 変換メソッド(Function)

ユーザーのリストから名前のリストを作る(または他の任意の変換を行う)メソッドです。

import java.util.List;
import java.util.ArrayList;
import java.util.function.Function;

public class UserMapper {
    public static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
        List<R> result = new ArrayList<>();
        for (User user : users) {
            result.add(mapper.apply(user));
        }
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 17),
            new User("Vika", 22)
        );

        // 名前のリストを取得
        List<String> names = mapUsers(users, user -> user.name);
        System.out.println(names); // [Anna, Boris, Vika]

        // 年齢のリストを取得
        List<Integer> ages = mapUsers(users, user -> user.age);
        System.out.println(ages); // [25, 17, 22]
    }
}

例 4: 値を供給するメソッド(Supplier)

必要になったときに値を取得したい場合があります — たとえば乱数を生成する、オブジェクトを作る、現在時刻を得るなど。Supplier<T> が適しています。

import java.util.function.Supplier;

public class ValueGenerator {
    public static int getValue(Supplier<Integer> supplier) {
        return supplier.get();
    }

    public static void main(String[] args) {
        // 乱数を取得
        int random = getValue(() -> (int)(Math.random() * 100));
        System.out.println("乱数: " + random);

        // 現在時刻(ミリ秒)を取得
        long time = getValue(System::currentTimeMillis);
        System.out.println("時刻: " + time);
    }
}

5. まとめアプリ: 例をつなげる

簡単な「ユーザー一覧」アプリを作るとしましょう。ここまでで次のことができるようになりました:

  • 条件でユーザーをフィルタリングする(例: 成人のみ);
  • ユーザーを別のものに変換する(名前、e-mail、年齢など);
  • 各ユーザーに対して処理を実行する(表示、ボーナス付与など);
  • 必要に応じて値を生成する(例: 新規ユーザーを作成)。

これらのメソッドを組み合わせれば、毎回コードを書き換えることなく、柔軟なデータ処理シナリオを構築できます。

import java.util.*;
import java.util.function.*;

public class UserApp {
    static class User {
        String name;
        int age;
        User(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public String toString() {
            return name + " (" + age + ")";
        }
    }

    // ユーザーを処理する汎用メソッド
    static void processUsers(List<User> users, Consumer<User> action) {
        for (User user : users) action.accept(user);
    }

    // 汎用フィルタ
    static List<User> filterUsers(List<User> users, Predicate<User> condition) {
        List<User> result = new ArrayList<>();
        for (User user : users) if (condition.test(user)) result.add(user);
        return result;
    }

    // 汎用変換
    static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
        List<R> result = new ArrayList<>();
        for (User user : users) result.add(mapper.apply(user));
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 17),
            new User("Vika", 22)
        );

        // 1. すべてのユーザーを表示
        processUsers(users, user -> System.out.println("ユーザー: " + user));

        // 2. 成人だけを抽出
        List<User> adults = filterUsers(users, user -> user.age >= 18);
        System.out.println("成人: " + adults);

        // 3. 成人の名前を取得
        List<String> adultNames = mapUsers(adults, user -> user.name);
        System.out.println("成人の名前: " + adultNames);

        // 4. 成人にボーナスを付与
        processUsers(adults, user -> System.out.println(user.name + " はボーナスを受け取りました!"));
    }
}

なぜ「従来の」やり方より良いのか?

この方法は柔軟性があります。同じメソッドを、渡す関数を変えるだけでさまざまなシナリオに使えます。コードが「この条件でフィルタ」「あの方法で出力」といった無限のバリエーションに膨れ上がることはありません。どこでも使える共通の道具が手に入り、結果としてコンパクトで分かりやすく、バグも入りにくくなります。さらに、このスタイルは最新の Stream API と非常に相性が良く、ストリームに移行しても学び直す必要はありません — アプローチは同じです。

6. 関数をパラメータとして渡すときの典型的なミス

ミス №1: シグネチャが期待されるインターフェースと一致しない。
メソッドが Predicate<User> を受け取るのに、戻り値が boolean ではなく String などのラムダを渡した場合、コンパイラはすぐにエラーを報告します。戻り値と引数が期待に合っているか確認しましょう。

ミス №2: ラムダ内で変更され得る変数を使ってしまう。
Java のラムダ式は外側のコンテキストから final または effectively final な変数だけを使用できます。そのような変数をラムダ内で変更しようとすると、コンパイルエラーになります。

ミス №3: インターフェースの取り違え。
たとえば Consumer<T> を渡すべきなのに、うっかり何かを返す関数(Function<T, R> など)を書いてしまうことがあります。ラムダが必要なものをちょうど返しているか(あるいは Consumer の場合は何も返さないか)確認しましょう。

ミス №4: 引数のラムダが複雑すぎる。
ラムダが 1~2 行を超えるなら、変数やメソッドに切り出したほうがよいでしょう。そうしないとコードが読みにくくなり、保守が難しくなります。

1
タスク
JAVA 25 SELF, レベル 49, レッスン 2
ロック未解除
動物のパレード:名前の長さで整列 🐘
動物のパレード:名前の長さで整列 🐘
1
タスク
JAVA 25 SELF, レベル 49, レッスン 2
ロック未解除
星のモニタリングの指標レポート 🔭
星のモニタリングの指標レポート 🔭
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION