いよいよラムダ式を実際に使っていきます。またJava8での追加内容のもう一つの目玉、ストリーム(Stream)に触れていきます。
Iterable#forEachメソッドによるコレクションの反復処理
コレクションの反復処理は、古くは
Enumeration
インタフェース、その後Iterator
インタフェース、もしくは普通にインデックスを使ったfor/while文で行っていました。例を書くまでもないですね。Java5で
java.lang.Iterable
インタフェース(utilではない)と拡張for文が導入され、シンプルに記述できるようになりました。List<string> names = new ArrayList<string>(); names.add("すず"); names.add("モモ"); names.add("ねね"); for (String name : names) { System.out.println(name); }
Java8では、
Iterable#forEach
というデフォルトメソッドが追加されました。定義は以下の通りです。default void forEach(Consumer<? super T> action);
Iterable
はList,Set,Queue,Deque
といったおなじみのインタフェースが継承しており、これらを実装するサブクラスで利用できます。また引数に関数型インタフェースの
Consumer
を取るので、ラムダ式を渡すことができます。上のfor文は以下のように書けます。
names.forEach(name -> System.out.println(name)); //さらにこの例では前回説明したメソッド参照を使うと以下のように書ける names.forEach(System.out::println);
キーと値からなる
Map
インタフェースはIterable
のサブインタフェースではありませんが、一貫性のためにMap#forEach
メソッドが実装されています。default void forEach(BiConsumer<? super K, ? super V> action);
引数の
BiConsumer
インタフェースの部分が分かりづらいですが、引数を2つ、キーと値を取って戻り値voidの処理を渡すことができます。Map<String, String> breed = new HashMap<>(); breed.put("すず", "茶トラ"); breed.put("モモ", "白茶トラ"); breed.put("ねね", "三毛"); breed.forEach((name, hair)-> System.out.println(name + "の毛色は" + hair));
実行結果
モモの毛色は白茶トラ ねねの毛色は三毛 すずの毛色は茶トラ
反復処理の制御が全て内部で行われ、プログラマは処理の記述に集中できます。
ちなみにbreakはどうすればいいのかというと、ラムダ式内でreturnすると次のループに移ります。ただフィルタリングをしたい場合はこの後説明するストリームAPIを使う方が良いでしょう。
ストリームAPIとパイプライン
ストリームAPIは、コレクション、配列、I/Oリソースなどのデータソースを元に各種の操作を行うAPIです。ストリームはある集計結果をまた次のデータソースとして渡すことができるので、処理をどんどん繋いで求める結果を得ることができます。
例えば「文字列の配列に対して、各要素を大文字にし、昇順にソートして出力する」という処理は以下のように書けます。
//ソース List<String> list = Arrays.asList("bb", "aa", "cc"); //従来のやり方(配列のままやれというのは無しで) List<String> result = new ArrayList<>(); for(String s : list){ result.add(s.toUpperCase()); } Collections.sort(result); for(String s : result){ System.out.println(s); } //ストリームAPI list.stream() .map(s -> s.toUpperCase()) .sorted() .forEach(System.out::println);
Collection#stream
メソッドでコレクションからStream
を得ることができます。他にも様々なソースからStreamを得る方法が用意されています。得られた
Stream
に対し、map,sorted,forEachと次々と操作を呼び出して処理を完了しています。これをストリームのパイプライン処理と呼びます。パイプライン処理には、処理の対象となるデータソースが必要です。データソースからストリームを生成して処理を呼び出しますが、処理には加工やフィルタリングといった、さらに後続の処理を期待する中間操作と、何かしらのアクションを実行したり別のソースへ出力するなどの終端操作があります。
ストリームの生成
//Collection#stream List<String> list = Arrays.asList("a", "b", "c"); Stream<String> stream1 = list.stream(); //Arrays#stream int[] intarray = {1, 2, 3}; IntStream stream2 = Arrays.stream(intarray); //Stream#of Stream<String> stream3 = Stream.of("abc", "def"); LongStream stream4 = LongStream.of(1L, 2L, 3L);
ストリームの生成にはいくつかのやり方が用意されていますが、基本は
Collection#stream
かStream#of
だと思います。通常の配列からストリームを生成するArrays#stream
もあります。関数型インタフェースと同じように、ストリームにもプリミティブ用の
IntStream
,LongStream
,DoubleStream
が用意されています。オートボクシングによる性能劣化を防ぐため、プリミティブ型を扱う場合は専用のストリームを使いましょう。中間操作
ストリームに対し何らかの手を加えた上で新しいストリームを生成する操作です。
「手を加えた上で」としてますが実際には即時に実行されるわけではないのですが、それについては後述します。
・filter
文字通り何かしらの条件によって要素を抽出します。引数にはbooleanを返す関数型インタフェースの
Predicate
を取ります。Stream<T> filter(Predicate<? super T> predicate); stream.filter(s -> !s.startsWith("n"));
・distinct
重複する要素を取り除きます。
Stream<T> distinct(); List<String> list = Arrays.asList("aA","AA","Aa", "Aa", "AA"); list.stream() .distinct() .forEach(System.out::println);
実行結果
aA AA Aa
・map
引数に
Function
を取り、戻り値からなる新しいストリームを返します。<R> Stream<R> map(Function<? super T, ? extends R> mapper)
//Stream<String> → Stream<String> Stream<String> s1 = Stream.of("naoki", "akiko", "ami"); Stream<String> s2 = s1.map(s -> s.toUpperCase()); //Stream<String> → Stream<Integer> Stream<String> s3 = Stream.of("naoki", "akiko", "ami"); Stream<Integer> s4 = s3.map(s -> s.length()); //ストリームの型変換 Stream<String> s5 = Stream.of("naoki", "akiko", "ami"); IntStream is = s5.mapToInt(s -> s.length());
Function
なので戻り値の型は何でもよく、異なるデータを持つストリーム(型パラメータが異なる)にすることもできます。ただし型自体は変換できないので、
Stream
からIntStream
へ変換する場合は専用のメソッドを使います。例に挙げたStream#mapToInt
、逆はIntStream#mapToObj
といったものがあります。さらにIntStream
→Stream<Integer>
のようにラッパークラスのストリームへ変換するboxed
メソッドなんてのもあります。・flatMap
多分とても分かりづらいのが
flatMap
です。<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
もう定義からして複雑ですが(;´∀`)
flatMapは後の回でもう少し詳しくとりあげますが、簡単に説明しておきます。
ポイントは
ストリームの各要素に対して何らかの処理をした結果、得られるのがストリーム、つまり複数の値を持つ集合になる場合に使います。
そのままだとストリームがたくさんできてしまいますが、
flatMapは後の回でもう少し詳しくとりあげますが、簡単に説明しておきます。
ポイントは
Function
の2つ目の型パラメータ=戻り値の型がStream
になっているところです。ストリームの各要素に対して何らかの処理をした結果、得られるのがストリーム、つまり複数の値を持つ集合になる場合に使います。
そのままだとストリームがたくさんできてしまいますが、
flatMap
は最終的に得られた全てのStream
を展開して1つのStream
に平坦化してくれます。それがflatと付いている意味です。List<Integer> list1 = Arrays.asList(1, 2, 3); List<Integer> list2 = Arrays.asList(4); List<Integer> list3 = Arrays.asList(5, 6); List<List<Integer>> src = Arrays.asList(list1, list2, list3); src.stream() .flatMap(list -> list.stream()) .forEach(System.out::print);
実行結果
123456
・peek
デバッグ用のメソッドです。ストリームの要素に対し引数の
Consumer
を実行しますが、これは値を返さないので、peek
自体の戻りは元のストリームの内容と同じです。Stream<T> peek(Consumer<? super T> action)
要素がパイプラインを通過する際にその内容を確認するといった使い方をします。
Stream.of("one", "two", "three", "four") .filter(e -> e.length() > 3) .peek(e -> System.out.println("フィルタ後: " + e)) .map(String::toUpperCase) .peek(e -> System.out.println("マップ後 : " + e)) .forEach(System.out::println);
実行結果
フィルタ後: three マップ後 : THREE THREE フィルタ後: four マップ後 : FOUR FOUR
実行結果がちょっと想像と異なるかもしれません。もし各中間操作が順番に実行されていくなら、フィルタ後の全要素→マップ後の全要素→最後の出力の順になるところですが、そうなっていませんね。
これはストリームAPIの処理は遅延実行※するように作られているためで、受け取ったラムダ式は
filter
やmap
などの呼び出し時点では実行されず、何を行うかという情報だけをパイプラインで繋いでいきます。そしてこの後説明する終端操作が呼ばれた時に初めて実行されます。上記の例で、
forEach
をコメントアウトするとpeek
内のラムダ式は実行されず、コンソールには何も出力されず終わります。なぜそうなっているかというと、効率とか性能とか、これはまた別の回で説明する並列ストリーム実現のためで、内部ではかなり複雑なことをやりながら、いい感じに結果を出してくれるというわけです。
※遅延評価と書かれていることも多いですが、あまりよろしくないようなので遅延実行としています。
・limit,skip
limit
は要素を引数の個数に制限、skip
は要素の先頭から引数の個数分スキップすします。コードは略!終端操作
だいぶ長くなってきましたが、もう少し行きます。
前述の通り、終端操作が呼ばれて初めてストリームに対する操作が実行されます。
終端操作は、すでに例として出している受け取った
Consumer
を実行するだけで何も返さないforEach
の他は、Stream
以外の何かしらの値を返すものとなっています。・count
まず単純なものから。
count
は要素の個数を返します。特に例も不要でしょう。long count()
・max,min
引数で渡された
Comparator
に従って、最大/最小の要素を返します。Optional<T> max(Comparator<? super T> comparator); Optional<T> min(Comparator<? super T> comparator);
ここで初出となる
java.util.Optional
ですが、これはJava8で導入されたクラスで、「nullかもしれない値を格納するコンテナ」です。詳細また別の回に(多いな・・・)。max,min
では、要素が0だった場合に空の(emptyな)Optional
が返されます。・allMatch,anyMatch,noneMatch
それぞれ引数で渡された
Predicate
の条件に対し、全て一致/いずれかが一致/全て一致しないかどうかをbooleanで返します。boolean allMatch(Predicate<? super T> predicate) boolean anyMatch(Predicate<? super T> predicate) boolean noneMatch(Predicate<? super T> predicate)
・findFirst,findAny
要素の中で最初の要素、任意の要素を取得します。
最初の要素は分かりやすいと思いますが、「任意の要素」ってなんぞ?と思われるでしょう。実際「こうだ!」という事例を示すことができなくて申し訳ないのですが(;´∀`)
両者の結果が変わってくるのは、まだ説明していない並列ストリームを使用した場合です。
OptionalInt result = IntStream.range(0, 100) .parallel() //並列ストリーム化 .filter(n -> n%2==0) .findAny(); System.out.println(result);
この結果は実行する度に変わり、自分の環境では
OptionalInt[26]
かOptionalInt[50]
になります。filter
を挟んでいるのは処理を入れることでばらつきやすくするためで、filter
をコメントアウトしても何度も実行すればばらけます(ほとんど50だがたまに25になりました)。内部でストリームが分割されて並列に処理されているためと思われます。
例えば、
int
の要素に対して「50以上のもののうちどれか1つを得る」という状況でfindAny
を使った場合、順次(sequential)ストリームでは最初に見つかった要素(findFirst
と同じ)となりますが、並列(parallel)ストリームでは、分割されて並行に処理されたストリームの中で一番最初に見つかったものが返されます。並列の度合いは環境によって異なるので結果は不定になりますが、順序を気にしなくて良いのであれば並列化ストリームを利用しつつ
findAny
を使った方が速く結果を得られるということでしょう。・toArray
ストリームの要素を配列に変換します。
IntFunction
を取る方は、配列コンストラクタ参照を渡すことで簡潔に記述できます。Object[] toArray() <A> A[] toArray(IntFunction<A[]> generator) Person[] men = people.stream() .filter(p -> p.getGender() == MALE) .toArray(Person[]::new);
・reduce
引数の
BinaryOperator
によって要素を集約し、結果を返します。このreduceと次のcollectはちょっと分かりづらいです(;´∀`)T reduce(T identity, BinaryOperator<T> accumulator) Optional<T> reduce(BinaryOperator<T> accumulator) <U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
引数の違いで3つあります。1つめの例は以下の通り。
int result = IntStream.of(10, 20, 30) .reduce(0, (a, b) -> a + b); //これでも同じ //.reduce(0, Integer::sum); System.out.println(result);
実行結果
60
最初は初期値と1つめの要素、次はその結果と2つめの要素・・・というふうに順番に
BinaryOperator
を呼び出していき、最終的に1つの結果を返します。要素が無い場合は初期値をそのまま返します。
引数が1つのものは初期値を与えないバージョンで、要素が無い場合に対応するため戻り値の型が
Optional<T>
となっています。問題は3つめですが、引数に
BinaryOperator<U> combiner
が追加されています。これは並列ストリームの時に意味を持つものなので、別の回で詳しく触れます。簡単に説明すると、並列ストリームの場合は要素をいくつかに分解して並列に
accumulator
を呼び出していきますが、その結果は最終的に1つにまとめる必要があります。その時にcombiner
が使われます。・collect
一番ややこしいのがこのcollectです。2種類用意されています。
一番ややこしいのがこのcollectです。2種類用意されています。
<R,A> R collect(Collector<? super T,A,R> collector) <R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
1つめからしてまず大変なのですが(;´∀`)
引数は
java.util.stream.Collector
型です。これは関数型インタフェースではないのでラムダ式は渡せませんが、様々な実装を取得できるjava.util.stream.Collectors
クラスが用意されているのでこれを使います。String str = Stream.of("すず", "もも", "まお", "ねね") .collect(Collectors.joining("/")); System.out.println(str);
実行結果
すず/もも/まお/ねね
Collectors#joining
はストリームがString
の時のみ利用可能で、要素の文字列を連結します。オーバーライドされていて3種類あり、例は引数をデリミタとして連結します。その他のメソッドはAPIドキュメント→Collectors (Java Platform SE 8 )をご覧下さい。
長くなってきているので回を分けて説明します。
そして引数3つ取る方ですが、reduceと同じく並列ストリームの場合に生きてくるので、並列ストリームの回に会わせて説明します。
【注意】ストリームを使い回してはいけない
mapの例であげたコードでは、毎回
Stream#of
でストリームを作り直していました。これを無駄だからと、以下のようにすると例外が発生します。Stream<String> s1 = Stream.of("naoki", "akiko", "ami"); Stream<String> s2 = s1.map(s -> s.toUpperCase()); Stream<Integer> s4 = s1.map(s -> s.length());
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed at java.util.stream.AbstractPipeline.<init>(AbstractPipeline.java:203) at java.util.stream.ReferencePipeline.<init>(ReferencePipeline.java:94) at java.util.stream.ReferencePipeline$StatelessOp.<init>(ReferencePipeline.java:618) at java.util.stream.ReferencePipeline$3.<init>(ReferencePipeline.java:187) at java.util.stream.ReferencePipeline.map(ReferencePipeline.java:186) at test.Main.main(Main.java:14)
ストリームの各操作では毎回
Stream
オブジェクトを返しますが、すでに何かしらの操作を行ったオブジェクトにもう一度操作を行うとIllegalStateException
が発生します。まあメソッドチェーンで文字通り流れるように書いていくのがストリームだと思うので、いちいち変数に入れて・・・とやらなければ問題無いでしょうが、念のため。
おしまいのひとこと
今回からOracleの公式サイトにあるテスト内容チェックリストと、その英語版の解説記事を元にしてみました。
「ラムダ式を使用してコレクションをフィルタリングする」「ストリーム上での遅延操作」は、collectの補足と合わせて次回にします。
2016年中の試験代割引キャンペーンには間に合わなそうですが、必ず記事を完遂して試験を受けるので、Java8の新機能に興味のある方はお付き合いいただければと思います。一連の記事は「JavaSE8Gold」ラベルを付けていきます。
それではみなさまよきガジェットライフを(´∀`)ノ