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 行を超えるなら、変数やメソッドに切り出したほうがよいでしょう。そうしないとコードが読みにくくなり、保守が難しくなります。
GO TO FULL VERSION