Function インターフェース

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • Function<T, R>T を受け取って R を返す 関数型インタフェース
  • 抽象メソッドは R apply(T t) 1 つだけ
  • andThen compose で関数合成が可能 ─ f.andThen(g).apply(x) = g(f(x))
  • ラムダ x -> x.length() をそのまま Function<String, Integer> に代入できる

Function インターフェース とは

Java の代表的な関数型インタフェース Function<T, R> を取り上げ、ラムダ式で値を変換する関数を作って apply で呼び出すコードを書いてみよう。

Function インターフェースとは何か

Java の Stream を勉強し始めると、filter には Predicatemap には FunctionforEach には Consumer といった具合に、たくさんの「関数型インタフェース」が登場します。その中でも、業務コードで最もよく目にするのが今回扱う java.util.function.Function<T, R> です。

Function<T, R> を一言でいうと「T 型の値を 1 つ受け取って、R 型の値を 1 つ返す関数」を表すインタフェースです。たとえば Function<Integer, Integer> なら「整数を受け取って整数を返す関数」、Function<String, Integer> なら「文字列を受け取って整数を返す関数」を意味します。Java は静的型付け言語なので、こうやって入力と出力の型を <> で明示することで、コンパイラに型安全性を保証してもらえるわけです。

「関数を変数のように扱う」という考え方は、Python の関数オブジェクトや JavaScript のアロー関数と同じ発想です。Java では Function という型を介してこれを実現しているのがポイントで、関数を変数に入れたり、メソッドの引数として渡したりできるようになります。

本レッスンでは、Function の基本的な作り方から、便利な compose andThen によるチェーン、そして identity という小ワザまで、業務で本当に使う知識だけをぎゅっと詰め込みます。今回の課題自体はシンプルですが、その背景にある「関数を値として扱う」考え方をしっかり身につけてください。

apply(T)Function の本体

Function<T, R> のメソッドは R apply(T t) ただ 1 つだけです。apply を呼ぶことで実際の変換処理が走り、戻り値として R 型の値が返ってきます。ラムダ式で書くと、次のようにスッキリ作れます。

Java

import java.util.function.Function; public class Demo { public static void main(String[] args) { Function<Integer, Integer> doubler = x -> x * 2; System.out.println(doubler.apply(5)); // 10 System.out.println(doubler.apply(0)); // 0 System.out.println(doubler.apply(-3)); // -6 } }

Function<Integer, Integer> doubler = x -> x * 2; の行で、「整数を受け取って 2 倍した整数を返す関数」を作って doubler という変数に入れています。あとは doubler.apply(5) のように呼び出すだけで、x * 2 が走って 10 が返ってきます。doubler は値であると同時に「関数」でもあるわけです。

ここで重要なのは、Function<Integer, Integer><Integer, Integer>左が引数の型、右が戻り値の型 という順番です。Function<入力, 出力> と覚えてしまえば順番を迷うことはありません。混同しがちなのが BiFunction<T, U, R> で、こちらは「TU を受け取って R を返す」二引数版なので、引数が 2 つほしいときに使います。

関数型インタフェースは「メソッドがちょうど 1 つだけのインタフェース」と定義されています。apply accept get test など名前は違っても、抽象メソッドが 1 つなのでラムダ式や メソッド参照 を渡せる、というルールです。@FunctionalInterface という注釈が付いていれば、コンパイラが「メソッドは 1 つだけ」を保証してくれます。

ラムダ式とメソッド参照

Function を作る方法はラムダ式以外にもいくつかあります。Java らしい書き方を 3 つ並べてみます。

Java

// 1. ラムダ式 (最もよく使う) Function<Integer, Integer> doubler = x -> x * 2; // 2. ブロック付きラムダ式 (複数行のロジックを書きたい時) Function<Integer, Integer> doublerVerbose = x -> { int result = x * 2; return result; }; // 3. メソッド参照 (既存のメソッドを使い回したい時) Function<String, Integer> parser = Integer::parseInt;

3 番目の Integer::parseInt がメソッド参照で、s -> Integer.parseInt(s) というラムダ式と同じ意味です。String を受け取って int (オートボクシングで Integer) を返すメソッドなので、Function<String, Integer> にぴったりはまります。実務では、既存のメソッドをそのまま渡せるならラムダ式より メソッド参照 の方が読みやすいので、よく好まれます。

composeandThen でチェーンする

Function の真価が発揮されるのが、複数の関数を組み合わせる場面です。たとえば「文字列を整数に変換して、その後 2 倍する」という処理は、関数を 2 つに分けて andThen で連結すると次のように書けます。

Java

Function<String, Integer> parse = Integer::parseInt; Function<Integer, Integer> doubler = x -> x * 2; Function<String, Integer> parseAndDouble = parse.andThen(doubler); System.out.println(parseAndDouble.apply("7")); // 14

parse.andThen(doubler) は「まず parse を実行して、その結果を doubler に渡す」という意味の新しい Function を作ります。逆向きの compose もあり、doubler.compose(parse) と書いても同じ結果になりますが、引数の流れの向きが逆です。

diagram (will load when visible)

図にすると一目瞭然ですが、andThen は「左の関数を先に実行する」、compose は「右の関数を先に実行する」という覚え方になります。実務では andThen の方が「上から順に読める」ので圧倒的によく使われます。compose を選ぶのは、数学の関数合成 f(g(x)) の書き方に揃えたいときや、すでにある関数の前段にもう 1 段処理を足したいときくらいです。

andThencompose はどちらも「新しい Function を返す」だけで、元の Function 自体は書き換わりません。Java の関数型 API は基本的にイミュータブルで、元の関数を破壊しないのが流儀です。

Function.identity() という小ワザ

Function には identity() という不思議な静的メソッドがあります。これは「受け取った値をそのまま返す関数」を作ってくれるメソッドで、ラムダ式で書くと x -> x と同じです。

Java

Function<String, String> same = Function.identity(); System.out.println(same.apply("hello")); // hello

「そんな関数いつ使うの?」と思うかもしれませんが、これが実は便利な場面が多いです。たとえば StreamCollectors.toMap でキーをそのまま値として使いたいとき、Collectors.toMap(Function.identity(), String::length) のように書きます。x -> x と書いても動きますが、Function.identity() の方が意図がはっきり伝わるので、コードレビューでも好まれます。

また、andThencompose で関数チェーンを組み立てるときの「とりあえず素通しのデフォルト値」としても重宝します。条件によって前段の関数を切り替える設計を取るときに、Function.identity() を差し込んでおくと「何もしない選択肢」を表現できて読みやすくなります。

よくある間違い

ここで、初学者が Function で踏みがちな落とし穴を 3 つ紹介します。

  • FunctionConsumer を混同するFunction<T, R> は「値を返す」関数、Consumer<T> は「値を受け取って戻り値なし (void) 」の関数です。System.out.println を渡したいだけなら Consumer 、結果を返してほしいなら Function 、と用途で選び分けます。間違って Function<Integer, Void> のような無理な型を作る前に、Consumer の存在を思い出してください
  • ラムダ式の型推論に失敗するvar f = x -> x * 2; のように var でいきなり受けようとすると、コンパイラが「ラムダ式単体では型を決められない」と言ってエラーになります。必ず Function<Integer, Integer> f = x -> x * 2; のように 左辺で型を宣言 するか、メソッド引数として渡してその場で文脈から型を決めるかのどちらかにします
  • null 引数を素通しさせるFunction<String, Integer> parser = Integer::parseInt;null を渡すと NullPointerException が出ます。Function 自体は null を弾く仕組みを持たないので、null の可能性があるときは事前に null チェックするか、Optional<T> を組み合わせるかで身を守る必要があります

もう 1 つだけ補足すると、Function<T, R> のジェネリクスに プリミティブ型 (int double 等) は直接書けません。Function<int, int> はコンパイルエラーで、ラッパー型の Function<Integer, Integer> を使う必要があります。int 専用に最適化された IntFunction<R>IntUnaryOperator という別インタフェースが用意されているので、パフォーマンスが効いてくる場面ではそちらを検討してください。

Function<Integer, Integer> のように同じ型を出し入れする場合に限り、UnaryOperator<Integer> という短い別名のインタフェースもあります。UnaryOperator<T>Function<T, T> を継承していて、書き味は同じです。引数と戻り値の型が同じだと意図が読み取りやすいので、慣れてきたら使い分けてみましょう。

実務で遭遇するパターン

中級レッスンで学んだことが、実際の業務コードでどう登場するかを整理します。

  • Stream.mapusers.stream().map(User::getName)map の引数は Function
  • バリデーション関数の合成trim.andThen(toLowerCase).andThen(validate) でパイプラインを作る
  • 設定値の解決Function<String, String> resolver = key -> env.get(key) のような変換関数
  • Repository のクエリrepo.findBy(predicate, mapper)mapper 引数

コードレビューで指摘されがちなポイント

PR を出すとシニアから入りやすい指摘です。先回りで身につけておけばレビューが一発で通ります。

  • Function を独自インタフェースで再定義している ─ 標準の java.util.function.Function を使う
  • 戻り値が void なのに Functionvoid なら Consumer<T>、引数なし戻り値ありは Supplier<R>
  • ジェネリクスがプリミティブFunction<Integer, Integer> よりプリミティブ専用 IntUnaryOperator のほうが速い
  • Function.identity() を知らないx -> x の代わりに Function.identity() がコレクター系で使える

パフォーマンス考慮事項

  • ラムダ + Function はほぼ無料 ─ JIT 最適化される
  • プリミティブ専用IntFunction<R> ToIntFunction<T> などでボクシング回避
  • andThen / compose の合成 ─ メソッド呼び出しが 1 段増えるだけ、ほぼ無視できる
  • identity() は static 単一インスタンス ─ 何度呼んでも同じオブジェクト
この章のポイント

ここまでの要点 Function<T, R> は 1 引数 1 戻り値の関数型 IF。Stream や合成 (andThen / compose) と組み合わせて使う。プリミティブ専用も存在。

やってみよう

それでは今回の課題に進みましょう。やることは次のとおりです。

  1. Solution.applyDouble(int n) メソッドを完成させる
  2. メソッド内で Function<Integer, Integer> doubler = x -> x * 2; を宣言する
  3. doubler.apply(n) を呼び、戻り値の Integerint として return する
  4. ファイル先頭で import java.util.function.Function; を忘れずに書く

戻り値の型は int ですが、doubler.apply(n) が返すのは Integer です。int 変数に代入したり return する瞬間に、オートアンボクシング が自動で Integerint の変換をやってくれるので、明示的なキャストは不要です。return doubler.apply(n); と素直に書けば動きます。

テストでは applyDouble(5)10applyDouble(0)0applyDouble(-3)-6 を返すかをチェックします。負の数や 0 でも x * 2 の結果が正しく返るはずなので、特別な if 文で先に判定する必要はありません。シンプルにラムダ式で x * 2 を計算するだけで全テストが通ります。

慣れてきたら、Function<Integer, Integer> tripler = x -> x * 3; を別に作り、doubler.andThen(tripler).apply(5)30 が返ることを試してみてください。さらに Function.identity()compose で挟んでも結果が変わらないことを確かめれば、関数チェーンの感覚がぐっと身につきます。次のレッスンでは Predicate と組み合わせて条件付きの変換に挑戦していきます。

よくある質問

Q. abstract クラスと interface はどう使い分けますか?

A. 型の契約だけ定義するなら interface、共通実装も含めて部分的に書きたいなら abstract クラスです。Java 8 以降は interface も default メソッドで実装を持てるため、abstract の出番は減っていますが、フィールド共有が必要なら abstract が必要です。

Q. interface に変数を書けますか?

A. 暗黙的に public static final な定数だけ書けます。ミュータブルなフィールドは持てないため、状態を持たせたい場合は abstract クラスにしてください。定数置き場として使う場合も、専用クラス + private コンストラクタの方がベターな設計です。

Q. 1 つのクラスに複数の interface を実装できますか?

A. できます。implements A, B, C のようにカンマ区切りで列挙します。複数の default メソッドが同名で衝突した場合はコンパイルエラーになるため、override で明示的に解決します。多重継承の難しさをこの形で限定的に解放しているのが Java の設計です。

次のレッスン

次は Predicate インターフェース で、Predicate インターフェース を学びます。

事前確認 — 進む前に次の 3 つができることを確認しましょう。

  1. Function の要点を自分の言葉で説明できる
  2. このレッスンの最小コード (または操作手順) を見ずに書ける
  3. 練習問題やクイズで間違えた箇所を読み直して理解した

理解度チェック (30 秒)

Q. Function とは何か、1 文で説明してください。

この章のポイント

A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。

関連レッスン

要件

  1. ファイル先頭で import java.util.function.Function; を書くこと
  2. メソッド内で Function<Integer, Integer> doubler = x -> x * 2; のようにラムダ式で Function を作ること
  3. doubler.apply(n) を呼び出し、その戻り値を int として return すること

入出力例

test-cases.txt

applyDouble(5)10 applyDouble(0)0 applyDouble(-3)-6 applyDouble(100)200

ヒント

main.java
main.java
学習モード

メモ

Function インターフェース

⌘S で保存