Function インターフェース
このレッスンで分かること
Function<T, R>はT を受け取って R を返す関数型インタフェース- 抽象メソッドは
R apply(T t)1 つだけandThencomposeで関数合成が可能 ─f.andThen(g).apply(x)=g(f(x))- ラムダ
x -> x.length()をそのままFunction<String, Integer>に代入できる
Function インターフェース とは
Java の代表的な関数型インタフェース
Function<T, R>を取り上げ、ラムダ式で値を変換する関数を作ってapplyで呼び出すコードを書いてみよう。
Function インターフェースとは何か
Java の Stream を勉強し始めると、filter には Predicate 、map には Function 、forEach には 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> で、こちらは「T と U を受け取って R を返す」二引数版なので、引数が 2 つほしいときに使います。
関数型インタフェースは「メソッドがちょうど 1 つだけのインタフェース」と定義されています。
applyacceptgettestなど名前は違っても、抽象メソッドが 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> にぴったりはまります。実務では、既存のメソッドをそのまま渡せるならラムダ式より メソッド参照 の方が読みやすいので、よく好まれます。
compose と andThen でチェーンする
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")); // 14parse.andThen(doubler) は「まず parse を実行して、その結果を doubler に渡す」という意味の新しい Function を作ります。逆向きの compose もあり、doubler.compose(parse) と書いても同じ結果になりますが、引数の流れの向きが逆です。
図にすると一目瞭然ですが、andThen は「左の関数を先に実行する」、compose は「右の関数を先に実行する」という覚え方になります。実務では andThen の方が「上から順に読める」ので圧倒的によく使われます。compose を選ぶのは、数学の関数合成 f(g(x)) の書き方に揃えたいときや、すでにある関数の前段にもう 1 段処理を足したいときくらいです。
andThenとcomposeはどちらも「新しいFunctionを返す」だけで、元のFunction自体は書き換わりません。Javaの関数型 API は基本的にイミュータブルで、元の関数を破壊しないのが流儀です。
Function.identity() という小ワザ
Function には identity() という不思議な静的メソッドがあります。これは「受け取った値をそのまま返す関数」を作ってくれるメソッドで、ラムダ式で書くと x -> x と同じです。
Java
Function<String, String> same = Function.identity();
System.out.println(same.apply("hello")); // hello「そんな関数いつ使うの?」と思うかもしれませんが、これが実は便利な場面が多いです。たとえば Stream の Collectors.toMap でキーをそのまま値として使いたいとき、Collectors.toMap(Function.identity(), String::length) のように書きます。x -> x と書いても動きますが、Function.identity() の方が意図がはっきり伝わるので、コードレビューでも好まれます。
また、andThen や compose で関数チェーンを組み立てるときの「とりあえず素通しのデフォルト値」としても重宝します。条件によって前段の関数を切り替える設計を取るときに、Function.identity() を差し込んでおくと「何もしない選択肢」を表現できて読みやすくなります。
よくある間違い
ここで、初学者が Function で踏みがちな落とし穴を 3 つ紹介します。
FunctionとConsumerを混同する —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.map ─
users.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なのにFunction─voidなら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) と組み合わせて使う。プリミティブ専用も存在。
やってみよう
それでは今回の課題に進みましょう。やることは次のとおりです。
Solution.applyDouble(int n)メソッドを完成させる- メソッド内で
Function<Integer, Integer> doubler = x -> x * 2;を宣言する doubler.apply(n)を呼び、戻り値のIntegerをintとしてreturnする- ファイル先頭で
import java.util.function.Function;を忘れずに書く
戻り値の型は int ですが、doubler.apply(n) が返すのは Integer です。int 変数に代入したり return する瞬間に、オートアンボクシング が自動で Integer → int の変換をやってくれるので、明示的なキャストは不要です。return doubler.apply(n); と素直に書けば動きます。
テストでは applyDouble(5) が 10 、applyDouble(0) が 0 、applyDouble(-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 つができることを確認しましょう。
- Function の要点を自分の言葉で説明できる
- このレッスンの最小コード (または操作手順) を見ずに書ける
- 練習問題やクイズで間違えた箇所を読み直して理解した
理解度チェック (30 秒)
Q. Function とは何か、1 文で説明してください。
A. 本文の「このレッスンで分かること」または冒頭の説明文を見直し、自分の言葉で要約できれば OK。詰まったら本レッスンの最初の H2 セクションを読み返してみましょう。
関連レッスン
要件
- ファイル先頭で
import java.util.function.Function;を書くこと - メソッド内で
Function<Integer, Integer> doubler = x -> x * 2;のようにラムダ式でFunctionを作ること doubler.apply(n)を呼び出し、その戻り値をintとしてreturnすること
入出力例
test-cases.txt
applyDouble(5) → 10
applyDouble(0) → 0
applyDouble(-3) → -6
applyDouble(100) → 200