関数型インターフェース徹底解説
前回、TicketCraftの一覧処理をラムダ式とStream APIで書き直しました。
.filter(bug -> bug.getStatus().equals("open") && bug.getSeverity().equals("high"))
このラムダ式、実はPredicate<BugTicket>という型の変数に代入できます。今回は、このラムダ式の「型」である関数型インターフェースを掘り下げ、条件やアクションを部品として使い回す方法を学びます。
関数型インターフェースは「白紙の依頼票」だと考える
関数型インターフェースとは、「抽象メソッドが1つだけ」 のインターフェースです。「何を渡すか(引数)」と「何を返すか(戻り値)」という依頼票のフォーマットだけが決まっていて、中身(実際の処理)はラムダ式で自由に書き込めます。
代表的な関数型インターフェースは次の4つです。
| インターフェース | 依頼票のフォーマット | TicketCraftでの用途例 |
|---|---|---|
Predicate<T> |
T型を受け取り、true/falseを返す |
「このチケットは高優先度か?」という条件判定 |
Consumer<T> |
T型を受け取り、何も返さない | 「このチケットの担当者に通知する」という処理 |
Function<T, R> |
T型を受け取り、R型を返す | 「このチケットから概要文字列を作る」という変換 |
Supplier<T> |
引数なしでT型を返す | 「新しい空のチケット下書きを作る」という生成 |
実践:条件とアクションを部品化する
Step 1: Predicateで条件を部品化する
前回のif (bug.getStatus().equals("open") && bug.getSeverity().equals("high"))を、名前のついた部品に分けます。
Predicate<BugTicket> isOpen = bug -> bug.getStatus().equals("open");
Predicate<BugTicket> isHighSeverity = bug -> bug.getSeverity().equals("high");
Predicate<BugTicket> isOverdue = bug -> bug.getDueDate().isBefore(LocalDate.now());
これらはand・or・negateで自由に組み合わせられます。
Predicate<BugTicket> needsEscalation = isOpen.and(isHighSeverity).and(isOverdue);
List<BugTicket> escalationTargets = allBugTickets.stream()
.filter(needsEscalation)
.collect(Collectors.toList());
ダッシュボード画面ではisOpen.and(isHighSeverity)だけを使い、督促バッチ処理ではneedsEscalationを使う、というように同じ部品を場面ごとに組み合わせを変えて再利用できます。
Step 2: Consumerでアクションを部品化する
Consumer<Ticket> logStatusChange = ticket ->
System.out.println("[LOG] " + ticket.getId() + " のステータスが変更されました");
Consumer<Ticket> notifyAssignee = ticket ->
System.out.println(ticket.getAssignee() + "さんに通知を送信しました");
Consumer<Ticket> onStatusChanged = logStatusChange.andThen(notifyAssignee);
onStatusChanged.accept(ticket); // ログ出力 → 通知の順で実行される
andThenは「まず1つ目、続けて2つ目」という実行順序で処理を連結します。順序を入れ替えると挙動が変わる点に注意が必要です。
Step 3: FunctionとSupplierも使ってみる
Function<Ticket, String> toSummaryLine = ticket ->
"[" + ticket.getStatus() + "] " + ticket.getTitle();
Supplier<TicketDraft> newDraftFactory = () -> new TicketDraft();
Functionは「変換」、Supplierは「(何も受け取らず)生成」を表します。表の通り、他の関数型インターフェースも「Function派生形」として捉えると覚えやすくなります。Predicate<T>は「Function<T, Boolean>」、Consumer<T>は「Function<T, Void>」のようなイメージです。
@FunctionalInterfaceアノテーション
@FunctionalInterface
public interface TicketValidator {
boolean isValid(Ticket ticket);
}
このアノテーションを付けると、「抽象メソッドが2つ以上追加されてしまった」といったミスをコンパイル時に検出できます。付けなくても1つだけなら関数型インターフェースとして使えますが、意図を明示するために付けておくのがおすすめです。
🧓 ベテランの現場コラム
Predicateを部品化しすぎてisOpen・isHighSeverity・isOverdue・isAssignedToMe・isRecentlyUpdated…と10個以上並ぶようになると、今度は「どれとどれを組み合わせれば目的の条件になるのか」を追うコストが増えます。部品化は「同じ条件を複数箇所で使い回すとき」に効果を発揮するものであり、1箇所でしか使わない条件まで無理に部品化する必要はありません。
🧓 ベテランの現場コラム
andThenで処理を連結するとき、「ログ出力→通知」のつもりが実は「通知→ログ出力」の順で書かれていた、という事故を見たことがあります。特に通知処理の中で例外が発生する可能性がある場合、後続の処理(ログ出力)が実行されないまま終わってしまうこともあるため、連結する処理の順序と、途中で失敗したときの挙動は必ずセットで考えましょう。
挫折ポイントTOP3と対処法
1. Predicate・Function・Consumerの違いを混同する
「戻り値がboolean?何か別の値?それとも何も返さない?」で機械的に判別しましょう。
2. andの合成順序を意識しない
a.and(b)とb.and(a)は結果が同じでも、途中で例外が起きる場合は評価順序が意味を持つことがあります。
3. 何でもかんでも関数型インターフェースの変数にしてしまう
1回しか使わない処理まで律儀にPredicateやConsumerの変数に切り出すと、かえって呼び出し元でその変数を探す手間が増えます。使い回す予定があるものだけを部品化しましょう。
この先、何を学べばいいか
Consumer<Ticket> notifyAssignee = ticket -> ...のようなラムダ式は、実は単に既存のメソッドを呼んでいるだけの場合、もっと短く書けます。次回はメソッド参照で、この書き方をさらに簡潔にします。
まとめ
- 関数型インターフェースは「抽象メソッドが1つだけ」の依頼票フォーマット
Predicate(条件)・Consumer(処理)・Function(変換)・Supplier(生成)が代表的な4種類and/or/negateやandThenで、条件・処理を部品として組み合わせられる- 使い回す予定がある処理だけを部品化する。1回限りの処理まで無理に切り出さない
🎓 理解度テスト
学んだ内容を確認しましょう。全5問。