Javaを書いていると単純な処理はずなのにやたらめったら,長いコードになることがある気がするので, Java8で導入されたFunctionalでイカれたメンバーを紹介するぜ!
Javaでコレクションの要素がある条件を満たすかどうか調べたいとき,コードがやたらめったら長くなることが多い気がします. たとえば数字のリストに「3」を含むか調べたいとき,べったべたに書くと以下のように書けます(普段自分はこんなコード書きませんよ!).
final List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,9);
boolean hasThree = false;
for(int i = 0; i < numbers.size(); i++) {
if(numbers.get(i).equals(3)) {
hasThree = true;
break;
}
}
assertThat(hasThree, is(true));
J2SE5.0以降,拡張forループが導入され,もうちょっと賢く以下のように書けるようになりました.
final List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,9);
boolean hasThree = false;
for(Integer number: numbers) {
if(number.equals(3)) {
hasThree = true;
break;
}
}
assertThat(hasThree, is(true));
このコードの何がイケてないって,先頭で3があるかどうかの変数を宣言・初期化しておかなければならないことだと思います. for文を使う以上スコープの関係で,for文前で変数宣言をする必要があるのでこれを取り除く必要があります. 今の場合はListのSuper ClassのCollections#containsメソッドを使うことにより,for文を取り除くことができます.
final List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,9);
final boolean hasThree = numbers.contains(3);
assertThat(hasThree, is(true));
ただし,もっと複雑な条件を課したいときにもはや,containsは使えません. たとえば「8よりの数を含むかどうか」を調べたいときはやはり,for文を使わざるを得ないでしょう.
final List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,9);
boolean hasNumebrGreaterThanEight = false;
for(Integer number: numbers) {
if(number > 8) {
hasNumebrGreaterThanEight = true;
break;
}
}
assertThat(hasNumebrGreaterThanEight, is(true));
Java8で追加されたStream APIを使うと,より簡潔かつ直感的なコードを書くことができます. これを使って「8よりの数を含むかどうか」を調べたいときは以下のようなコードで表現できます.
final List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,9);
boolean hasNumberGreaterThanEight = numbers.stream().filter(num -> num > 8).findAny().isPresent();
assertThat(hasNumberGreaterThanEight, is(true));
Stream API
はjava.lang.streamパッケージに定義されていて,コレクションに対してmapやreduceなどといった関数型プログラミングスタイルの操作が可能になるようなクラス群が定義されています.
filterメソッドはその名の通り,引数の条件が真になるものだけを取り出すフィルターの操作を行います. 引数には,コレクションの各要素を引数にとり,フィルタリングしたい条件を満たすかどうかの真理値を返す関数を渡します.
Javaでは関数は第一級オブジェクトではない(=生成・代入・演算などができない)ため,実際にはPredicate<T>
型のインスタンスを渡しますが,糖衣構文としてラムダ式
を用いることができます.実際先ほどのコードは以下のコードと同じです.
final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
boolean hasNumberGreaterThanEight =
numbers.stream().filter(new Predicate<Integer>() {
@Override
public boolean test(Integer num) {
return num > 8;
}
}).findAny().isPresent();
assertThat(hasNumberGreaterThanEight, is(true));
実装が必要なメソッドを1つ持つインターフェースを関数型インターフェース
とよびます.
上記のコードからわかるようにPredicate<T>
は入力値がある条件を満たすかを確認するために利用されるようなインターフェースで以下のように実装されています.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
数字のリストがあって,その和を求めたいなどということはよくあると思います. こういうとき結構コードが長くなりがちです.まあ普通に書くと次のような感じになるかと思います.
final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
Integer sum = 0;
for(Integer num: numbers) {
sum += num;
}
assertThat(sum, is(45));
個人的にはこの先頭にある合計値を格納する変数宣言・初期化と,for文がバラバラになっているのが気に食わないです. やりたいのは,リスト各要素の総和を求めたいという1つの作業なのに,これらを表現したコードがひとくくりまとまってに見えないからです.
ここでまたStream APIを用いることにより簡潔することができます.ストリームの各要素を畳み込むreduce関数が用意されています.使い方を説明するよりもコード見た方が理解が早いでしょう.以下のような具合です.
final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
Integer sum = numbers.stream().reduce((n,m) -> n+m).orElse(0);
assertThat(sum, is(45));
ストリームの要素がゼロだとそもそも畳み込むことができないため,reduceの結果は,値が存在するかしないかを表現するOptional型
のインスタンスとして値が帰ってきます.Optional#orElse(arg)は,値が存在なかった場合にargを使い,存在すればその値を返すという具合になります.以下のような感じ.
Optional<Integer> valA = Optional.empty();
assertThat(valA.orElse(0), is(0));
Optional<Integer> valB = Optional.of(100);
assertThat(valB.orElse(0), is(100));
さて「リストの5以下の要素を2倍して足し合わせたい」なんてときはどうしたら良いでしょうか. これはStream APIが使えないとなるとかなりダルいことになります.
final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
Integer value = 0;
for(Integer n: numbers) {
if(n <= 5) {
value += n * 2;
}
}
assertThat(value, is(30));
だいぶコードの意図がわかりにくくなってきました.
こういうときこそStream APIが役に立ちます.今回新たにStream#map
というメソッドを用います.
これは単純に「リストの各要素に操作を加えたリスト」を返す関数です.
実際には以下のように使います.
final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
final Integer value = numbers.stream()
.filter(n -> n <= 5)
.map(n -> n * 2)
.reduce((n,m) -> n + m)
.orElse(0);
assertThat(value, is(30));
ここでmapにまたもやラムダ式を渡していますが,このmapに渡るものはfilterで渡していたPredicateとは異なる,Function<T, R>というインターフェースを持ちます.Predicateは条件を満たすかどうかを判定するのに使われる関数であるのに対し,いまmapという操作はある型の値を任意の型の値に変換する操作であるので,Function<T, R>というように型パラメータが2つあります.Function<T, R>を使って先ほどのコードを書き換えてみると以下のようになります.
final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
final Integer value = numbers.stream()
.filter(n -> n <= 5)
.map(new Function<Integer, Integer>() {
@Override
public Integer apply(Integer n) {
return n * 2;
}
})
.reduce((n,m) -> n + m)
.orElse(0);
assertThat(value, is(30));
reduceに渡している関数についてもこれまででてきたFunction<T,R>,Predicateとは異なります.BinaryOperatorという関数型インターフェースになります.BinaryOperatorはBiFunction<T,T,T>を継承していてその中にabstractなapplyというメソッドが定義されています.reduceに渡すラムダ式は基本的にこのapplyをオーバーライドする形になります.
コレクションの各要素を出力するのはサンプルコードとかでよく見かけますね. 例によってJava < 1.8 っぽく書くと以下のような感じになると思います.
final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
for(Integer n: numbers) {
System.out.println(n);
}
Java8ではIterableインターフェースにforEachメソッドが追加されました. 例によって引数には各要素に対して加えたい処理を書きます.
final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
numbers.forEach(System.out::println);
ここでforEachの引数の型を見てみるとConsumerとなっています.
これはある型Tの引数を受け取り,副作用を起こす関数を受け取るものです.
したがって引数はn -> System.out.println(n)
とも書くことができますが,
より簡潔にメソッドの参照を渡すことができます.
Javaでこういうコードが書けてしまうのが新鮮ですね.