Stream API を割と使っている
今参画しているプロジェクトでは Java8 を使用しているのもあって遠慮無く Lambda Expression や Stream API を使用している。 特に Stream API は以前だと簡単な絞り込みをしたい場合でも for ループを回して面倒くさい書き方をしなければいけなかったのがシンプルに書けるようになっていて素晴らしいと思う。思うがやはり Scala なんかと比べると冗長に感じて仕方がないところはある……。
特に以下の collect(Collectors.toList())
は長い。もうすこし何とかならなかったのだろうかと感じる:
public static void main(String[] args) {
// 途中で List<E> にするのに boxing する必要があるのも地味に辛い
final List<Integer> list = IntStream.range(1, 11).map(x -> x * x).boxed().collect(Collectors.toList());
}
Scala だと以下で凄くシンプルで羨ましい:
val seq = 1.to(10).map(x => x * x).toSeq
まぁ無い物ねだりをしても仕方がない。Java で飯を食っている以上 Java の枠組みの中でなるべく綺麗なコードを書くように気をつけるしかない。
他の言語でよくやるメソッド内関数が簡単に書けるようになったけどやっぱり辛いという話
例えば PHP や Python などだと lambda 構文もあるし関数内で関数を定義する事も簡単なのでよく使う:
# f(x) 内でしか使用しないが何度も出てくるような関数を f(x) 内で g(y) として定義する事で影響範囲がわかりやすい
def f(x):
def g(y):
return y ** 2
return g(g(x) + 1)
これが Java でやろうとすると Java8 以前だと無名クラスを使用しなければならなくてこれがもうとんでもなく面倒くさかった、というかやろうとも思わなかった:
public static void main(String[] args) {
final Func<Integer, Integer> f = new Func<Integer, Integer>() {
@Override
public Integer apply(Integer x) {
return x * x;
}
};
System.out.println(f.apply(f.apply(1) + 1));
}
public static interface Func<X, R> {
R apply(X x);
}
Java8 からは Lambda Expression を用いて以下で書ける:
public static void main(String[] args) {
final Func<Integer, Integer> f = x -> x * x;
System.out.println(f.apply(f.apply(1) + 1));
}
public static interface Func<X, R> {
R apply(X x);
}
更にこのような 1 つの引数を受け取り 1 つの結果を返すような Interface は java.util.function
パッケージに用意されているのでわざわざ自分で Interface を定義しなくてもよい:
public static void main(String[] args) {
final IntUnaryOperator f = x -> x * x;
System.out.println(f.applyAsInt(f.applyAsInt(1) + 1));
}
OK. これはシンプル。素晴らしい。……とは言い難い。IntUnaryOperator
とはなにか?
FunctionalInterface 群の型が覚えられない...
Scala だと全部型推論できるので左辺は val
と書けば良い。つまり右辺がどうなっていようが関係ない:
val f = (_: Int) * (_: Int)
val g = (x: Int) => x * x
val h = () => print("Hello")
val i = (x: String) => print(s"Hello $x")
一方、これを Java8 で書こうとすると以下になる:
public static void main(String[] args) {
final IntBinaryOperator f = (x, y) -> x * y;
final IntUnaryOperator g = x -> x * x;
final Runnable h = () -> System.out.println("Hello");
final Consumer<String> i = x -> System.out.println(String.format("Hello %s", x));
i.accept("Baka");
}
この BinaryOperator
だとか Consumer
だとかを覚えないと書けない。
とりあえずひしだまさんの Functional Interface Memo を見て確認すると、以下の 5 つに大別されるのがわかる:
クラス名 | 概要 |
---|---|
Supplier | 引数はなく 1 つの結果を返す。供給する (supply) と覚える。 |
Consumer | 1 つの引数を受け取り何も返さない。消費する (consume) と覚える。2 つ受け取って何も返さない BiConsumer というのもある。 |
Predicate | 1 つの引数を受け取りその判定結果 (boolean) を返す。2 つの引数を受け取り判定する BiPredicate というのもある。Stream API の filter() に渡す際によく使う。 |
Operator | 引数の型と同じ型の戻り値を返す。引数の個数により UnaryOperator, BinaryOperator と用意されている。 |
Function | 引数の型と別の型の戻り値を返す。引数の個数により Function, BiFunction と用意されている。 |
番外で引数も戻り値もない、ただ単に決まった処理をしたいといったものは Runnable を使う。まぁこれはいいだろう。
上記 5 つの大別の中で更に Java には int, long, double, boolean 等のプリミティブ型があるので、その組み合わせぶん全て用意されている。
そこが便利ではあるが分かりにくくしている。例えば DoubleToLongFunction
とか IntBinaryOperator
とか、そういうものだ……。
しかし、定義しようとしている関数のシグネチャを考えて、上の表に当てはめてみれば多少は考えやすくなるのではないだろうか。つまり、
- 引数はあるか? -> 無いなら Supplier
- 戻り値はあるか? -> 無いなら Consumer
- 戻り値は boolean か (何らかの判定をしたいのか) ? -> そうなら Predicate
- 引数と戻り値の型が一緒か? -> そうなら Operator, そうでないなら Function
このチャートで適切なものが選択できるはずだ。