Java | Java 詳細・モダン文法:ラムダ式・関数型 – Function

Java Java
スポンサーリンク

Function を一言でいうと

java.util.function.Function<T, R> は、

「T 型の値を 1 つ受け取って、R 型の値を 1 つ返す“変換処理”を表す関数型インターフェース」

です。

数式で書くと f: T → R
コードで書くと「`T を受け取って R を返すメソッド」を、オブジェクトとして表現したもの」です。

  • String → Integer(文字列を数値に変換)
  • User → String(ユーザから表示名を取り出す)
  • Integer → Integer(数値を 2 倍にする)

といった「変換」や「マッピング」を、ラムダ式で扱いたいときの基本中の基本の型です。


Function の中身をまずしっかり掴む

シグネチャ(メソッド定義)を読む

Function<T, R> はざっくりこう定義されています。

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);            // 抽象メソッド(これが本体)

    // ほかに andThen, compose, identity など default メソッドがある
}
Java

一番大事なのはこの 1 行です。

R apply(T t);
Java

これを日本語にすると、

「T を 1 つ受け取って、R を 1 つ返すメソッド」

です。

つまり、Function<T, R> は「T -> R の関数の“型”」だと理解してください。

最もシンプルな使用例

例として、「Integer を 2 倍して Integer を返す関数」を書いてみます。

import java.util.function.Function;

public class FunctionBasic {
    public static void main(String[] args) {
        Function<Integer, Integer> doubleFunc = x -> x * 2;

        Integer result = doubleFunc.apply(10);   // 20
        System.out.println(result);
    }
}
Java

ここでの対応関係を整理します。

  • 型:Function<Integer, Integer>
    T = Integer, R = Integer の「Integer -> Integer」関数
  • ラムダ:x -> x * 2
    apply メソッドの中身をラムダで書いたもの
  • 呼び出し:doubleFunc.apply(10)
    → 「10 を渡して結果を返してもらう」

ラムダを見たら必ず、「どの関数型インターフェースに代入されているか(ここでは Function)」とセットで考える癖を付けてください。


典型的な変換例:String → Integer、User → String

例1:文字列を整数に変換する Function

import java.util.function.Function;

public class StringToInt {
    public static void main(String[] args) {
        Function<String, Integer> parseInt =
                s -> Integer.parseInt(s);

        System.out.println(parseInt.apply("123"));  // 123
    }
}
Java

Function<String, Integer> は「String -> Integer の関数」です。
apply("123") とすると、その変換結果が返ってきます。

メソッド参照を使ってもっと短く書くと、こうも書けます。

Function<String, Integer> parseInt = Integer::parseInt;
Java

Integer.parseInt(String) という既存メソッドを、そのまま Function として扱う」という書き方です。

例2:ユーザーオブジェクトから名前を取り出す Function

import java.util.function.Function;

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

public class UserNameMapper {
    public static void main(String[] args) {
        Function<User, String> toName =
                u -> u.name;

        User alice = new User("Alice", 20);
        String name = toName.apply(alice);  // "Alice"
        System.out.println(name);
    }
}
Java

Function<User, String> は「User -> String」。
つまり「User から String を取り出す」変換関数です。

こういう「オブジェクトから ある情報だけを取り出す」処理は、Stream の map などで頻出します。


Stream.map と Function の関係(ここはしっかり)

map は「要素を別の形に変換するための Function を受け取る」

Stream の map のシグネチャはこうです。

<R> Stream<R> map(Function<? super T, ? extends R> mapper)
Java

長くて怖そうですが、真ん中だけ見るとこうです。

Function<T, R> を渡すと、Stream<R> を返す」

つまり、「元の要素 T を R に変換する関数」を渡すメソッドです。

具体例で見ます。

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

public class MapExample {
    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob", "Charlie");

        Function<String, Integer> lengthFunc = s -> s.length();

        List<Integer> lengths =
                names.stream()
                     .map(lengthFunc)              // String -> Integer の Function
                     .collect(Collectors.toList());

        System.out.println(lengths); // [5, 3, 7]
    }
}
Java

map(lengthFunc) の部分で、

  • lengthFuncFunction<String, Integer>
  • namesStream<String>
  • 結果は Stream<Integer>

という対応になっているのが分かると思います。

直接ラムダで書くと、こうも書けます。

names.stream()
     .map(s -> s.length())
     .collect(Collectors.toList());
Java

この s -> s.length() は、
Function<String, Integer> として解釈されるラムダ式」です。


andThen / compose による「関数の合成」(超重要)

Function の強みの 1 つが、「関数どうしを合成できる」という点です。
andThencompose という default メソッドが用意されています。

andThen:まずこれをして、そのあとあれをする

f.andThen(g) は、「先に f を適用して、その結果に対して g を適用する」という合成関数を作ります。

式で書くと x -> g(f(x)) です。

例:「文字列を数値に変換してから 2 倍にする」

import java.util.function.Function;

public class AndThenExample {
    public static void main(String[] args) {
        Function<String, Integer> parseInt = Integer::parseInt; // String -> Integer
        Function<Integer, Integer> doubleFunc = x -> x * 2;     // Integer -> Integer

        Function<String, Integer> parseAndDouble =
                parseInt.andThen(doubleFunc);  // String -> Integer

        System.out.println(parseAndDouble.apply("10"));  // 20
    }
}
Java

ここで parseAndDouble は、「String → Integer の新しい Function」です。
中身は「Integer.parseInt してから x * 2 する」処理になっています。

流れを言葉にすると、

parseInt を適用 → 結果を doubleFunc に渡す」

です。

compose:g をしてから f をする(順番が逆)

f.compose(g) は、「先に g を適用して、その結果に対して f を適用する」合成です。
式で書くと x -> f(g(x))

さっきの例を compose で書いてみます。

Function<String, Integer> parseInt = Integer::parseInt;
Function<Integer, Integer> doubleFunc = x -> x * 2;

Function<String, Integer> parseAndDouble =
        doubleFunc.compose(parseInt);  // String -> Integer
Java

これも「文字列を数値にしてから 2 倍にする」関数です。

andThencompose の違いは、「どっちを先に適用するか」です。

  • f.andThen(g)g(f(x))(f のあと g)
  • f.compose(g)f(g(x))(g のあと f)

最初は混乱しやすいので、

「andThen は“and then …(そして次に)”と読むので、“自分のあとに” g が来る」
「compose は“g を先に適用して、そのあと自分(f)を適用する」

と自然言語ベースで覚えてしまうのもアリです。

条件付きで使える場面を意識する

関数の合成をちゃんと使うと、

「変換処理を小さなステップに分けて書き、それを組み合わせて新しい処理を作る」

というスタイルが取れます。

例えば、

  • User -> String(名前を取り出す)
  • String -> Integer(名前の長さを求める)

を別々の Function として定義し、
andThen で「User -> Integer」の関数を作る、といった使い方です。


Function.identity():入力そのまま返す関数

「なにもしない変換」が欲しいとき

Function.identity() は、「渡された値をそのまま返す関数」 を返します。

型は Function<T, T> です。

import java.util.function.Function;

public class IdentityExample {
    public static void main(String[] args) {
        Function<String, String> id = Function.identity();

        System.out.println(id.apply("hello"));  // "hello"
    }
}
Java

「何も変換したくないけど、Function<T, T> が必要」という場面で使います。

例えば、Collectors.toMap などで「キーは何かで変換、値はそのまま使う」
といったときに出てきます。


原始型用の Function(IntFunction など)

オートボクシングを避けるためのバリエーション

Function<T, R> は参照型用なので、
Function<Integer, Integer> のように書くと、int との間でオートボクシングが発生します。

大量の数値を扱う場面では、プリミティブ専用の関数型インターフェースを使う方が効率的です。

よく出てくるものとしては:

  • IntFunction<R>int -> R
  • LongFunction<R>long -> R
  • DoubleFunction<R>double -> R

例えば、「int を文字列に変換する」関数ならこうです。

import java.util.function.IntFunction;

public class IntFunctionExample {
    public static void main(String[] args) {
        IntFunction<String> toString =
                x -> "値: " + x;

        System.out.println(toString.apply(10));  // "値: 10"
    }
}
Java

Stream の IntStream.mapToObj などで出てきます。


Function を自作メソッドの引数として使う発想

「処理の中身」を外から注入できる

例えば、「リストの全要素に何らかの変換をかける」ユーティリティを自分で書きたいとします。

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

public class MyMapper {
    public static <T, R> List<R> mapList(List<T> list, Function<? super T, ? extends R> mapper) {
        List<R> result = new ArrayList<>();
        for (T t : list) {
            R r = mapper.apply(t);
            result.add(r);
        }
        return result;
    }

    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob");
        List<Integer> lengths = mapList(names, s -> s.length());
        System.out.println(lengths);  // [5, 3]
    }
}
Java

ここで mapList は、「変換の枠組みだけ」を担当しています。

  • List<T> を受け取る
  • 各要素に Function<T, R> を適用する
  • List<R> を返す

“どう変換するか”は外から受け取る。
“どうループして結果を集めるか”だけを mapList が担当する。

このように、Function を引数に取ると、「アルゴリズム」と「具体的な処理内容」をキレイに分離できます。

Stream API の多くは、まさにこういう思想で設計されています。


まとめ:Function を自分の中でこう位置づける

Function<T, R> を一文でまとめると、

「T を 1 つ受け取って R を返す“変換”を表す、Java の基本的な関数型インターフェース」

です。

特に押さえておきたいポイントは、

  • シグネチャは R apply(T t) → 「T -> R
  • map など「変換」を行うメソッドで多用される
  • andThen / compose で関数どうしを合成できる(処理を小さく分けて組み合わせられる)
  • Function.identity() で「そのまま返す」関数が手に入る
  • 原始型用に IntFunction などのバリエーションがある
  • 自作メソッドの引数に使うと、「枠組み」と「処理の中身」を分離できる

タイトルとURLをコピーしました