技術開発日記

技術やら日々思ったことを綴ってます。

関数型インターフェイスが多すぎる理由を調べてみた

Java8について調べると、標準で用意されている関数型インターフェイス関数の多さにびっくりする。
Supplier、Consumer、Predicate、Function、UnaryOperator、BinaryOperator、etc..
全部で40以上はある。

正直はじめはこれ全部覚えるのかと、かなり憂鬱だったが、おおまかに下記の4つの使い方が理解できれば特に問題ないと思っている。(他のはそれの派生にすぎないため)
・ Supplier
・ Consumer
・ Predicate
・ Function

ただ、なんでこんなに多いんだろうと思う人もいると思う。
少なくて、自分は思った。

例えば下記のコードを見てみる。

public static void main(String[] args) {
    // 文字数を返却するFunctionを定義
    Function<String, Integer> func = x -> x.length();

    // 出力
    System.out.println(func.apply("sample")); // 6
}

同じ内容をToIntFunctionでも再現できる。

public static void main(String[] args) {
    // 文字数を返却するFunctionを定義
    ToIntFunction<String> func2 = x -> x.length();

    // 出力
    System.out.println(func2.applyAsInt("sample")); // 6
}

同様にBiFunction,ToIntBiFunctionでも同じことが言える。

public static void main(String[] args) {
    // 文字数を足して返却するFunctionを定義
    BiFunction<String, String, Integer> func3 = (x, y) -> (x + y).length();
    ToIntBiFunction<String, String> func4 = (x, y) -> (x + y).length();

    // それぞれ出力
    System.out.println(func3.apply("sample1", "sample2")); // 12
    System.out.println(func4.applyAsInt("sample", "sample2")); // 12
}


この他にもToLongFunction、IntToDoubleFunction、といった全てFunctionで補えるのにわざわざ、別のInterfaceとして定義されている理由がまったくわからなかった。

そこで、いろいろ調べてみると同じ結論にたどり着く内容のものがありました。

Why doesn't Java 8's ToIntFunction<T> extend Function<T, Integer> - goto: answer

上記の疑問はなぜ、ToIntFunctionはFunctionを継承しないというものだが、確かによく考えてみると、下記のように継承しても問題ないはず。
わざわざ、applyAsIntなんてメソッドを追加する必要もないし、汎用性もある。

@FunctionalInterface
public interface ToIntFunction<T> extends Function<T, Integer> {
    // int applyAsInt(T value);

    @Override
    default Integer apply(T value) {
        return Integer.valueOf(applyAsInt(value));
    }
}

これはPredicateでも同様のことが言える。
これもtestというメソッドで区別せず、全てapplyで適用出来た方が汎用的だし、使いやすいはずなのにわざわざ、FunctionとPredicateを別物として定義している。

@FunctionalInterface
public interface Predicate<T> extends Function<T, Boolean> {
    // boolean test(T t);

    @Override
    default Boolean apply(T t) {
        return Boolean.valueOf(test(t));
    }
}


では、なぜ継承せず、まったく別のインターフェイスとしてJava8で定義したのか。

どうやら、その原因としてジェネリックスではprimitive型が使用できず、強制的にラッパークラスしか使えないことに起因しているらしい。

つまり、これは不可能だが

Function<String, int> myFunction;

これは可能ということ

Function<String, Integer> myFunction;

で、これの何が問題かというと primitive型とラッパーの参照型を利用すると boxing/unboxing といった変換処理が影響してくる。
boxing/unboxing はコストが高い上に、ラッパークラスのオブジェクト生成を効率化するために、primitive型を扱うアルゴリズムを複雑にしないといけないので非常に非効率。
なので、わざわざ、boxing/unboxing を必要としないインターフェイスをたくさん作ったようです。

この問題はPrimitive collections support breaks existing codeでも議論されていて
Brian Goetzというlambda projectのリーダーが下記のような内容を示していました。

primitive用のStreamにはトレードオフを伴うが、なるべくいい方向になうように検討している。

1.8種類全てのprimitive型に対応しない。
  int,long,doubleのみ対応した。
2.primitive型で利用されやすい計算(sorting,reduction) ができるようにprimitive型のStreamを定義。

また、将来(JDK 9)ではValue Typesによってこれらのinterfaceを取り除くことができるかもしれない。

まとめ

そんなわけで、インターフェイスの数がこんなにも多くなってしまった理由としては、実は結構単純でboxing/unboxingのコスト、効率化による問題に対応するためでした。
結果primitive型に対応する専用のインターフェイスがたくさん定義されたようです。

参考
http://stackoverflow.com/questions/22690271/why-doesnt-java-8s-tointfunctiont-extend-functiont-integer
http://stackoverflow.com/questions/22682836/why-doesnt-java-8s-predicatet-extend-functiont-boolean
http://gotoanswer.stanford.edu/?q=Why+doesn%27t+Java+8%27s+ToIntFunction%3CT%3E+extend+Function%3CT%2C+Integer%3E