本連載では、今までJavaの経験はあっても「ラムダ式は、まだ知らない」という人を対象にラムダ式について解説していきます。最終回は、Stream APIの特殊なメソッド3つと、ラムダ式と関係が深いメソッド参照やコンストラクター参照についてコード例を交えて解説。
前回の「Stream APIの主なメソッドと処理結果のOptionalクラスの使い方」では、Stream APIの持つ主なメソッドについて見てきました。今回は前回の記事では紹介できなかったStream APIのメソッドと、ラムダ式と関係が深い「メソッド参照」「コンストラクター参照」について見ていきます。
Stream APIには前回の記事で紹介したもの以外にもさまざまなものが用意されています。今回はStream APIの中でも使う際に注意をする必要があるメソッドを紹介します。
またJava 8では、ラムダ式とともにメソッドの呼び出しやコンストラクターの呼び出しを簡易化したメソッド参照やコンストラクター参照の表記法を導入しました。これらはラムダ式と同様に「関数型インターフェース」を引数として持つメソッドで使われる表記法です。
今までの連載ではCollectionや配列からStreamを生成していましたが、今回は要素を指定した方法で作成してStreamを生成するメソッドを見てみましょう。これらのメソッドは無制限に要素を作成するので、生成されたStreamから処理をする際に、要素数を何らかの方法で絞る必要があります。主なメソッドは次のものになります。
呼び出し元クラス/インターフェース | メソッド | 概要 |
---|---|---|
Stream<T> | generate(Supplier<T> supplier) | supplierが作成した値を無制限に要素として持つStreamインスタンスを生成 |
Stream<T> | iterate(T seed, UnaryOperator<T> operator) | Tを初期値の要素とし、その値を受け取ったUnaryOperatorが返す値を生成し、その値をまたUnaryOperatorが受け取り処理を行うようにと無制限に要素が作成されていくStreamインスタンスを生成 |
それではサンプルを見てみましょう。ここでは無制限に要素を作成するStreamを、Stream#limitメソッドで3個までしか要素を生成しないように制限をかけています。
Stream<String> stream1 = Stream.generate(()-> "あ"); Stream<Integer> stream2 = Stream.iterate(1, i -> ++i); stream1.limit(3).forEach(value -> System.out.println(value)); stream2.limit(3).forEach(value -> System.out.println(value));
あ あ あ 1 2 3
要素数を絞らずにこれらのメソッドを実行した場合、下記のように同じ結果が延々と続くことになるので注意してください。
Stream<String> stream1 = Stream.generate(()-> "あ"); stream1.forEach(value -> System.out.println(value));
あ あ あ あ ・ ・ ・
次に処理結果から集約処理を行うreduceメソッドについて見てみましょう。この処理は例えば、次のようなBigDecimalのListがあり全てのBigDecimalの合計を算出したい場合があるとします。
List<BigDecimal> list = new ArrayList<BigDecimal>(); list.add(new BigDecimal("1")); list.add(new BigDecimal("2")); list.add(new BigDecimal("3")); list.add(new BigDecimal("4")); list.add(new BigDecimal("5"));
次の図のように最初の2つの要素で指定された処理を行い、その結果と次の要素を使ってまた同じ処理を行います。それを繰り返していき、最終的な結果を取得するメソッドです。
また、reduceメソッドは3種類あり、それぞれ引数および戻り値が異なるので注意してください。
戻り値 | メソッド | 概要 |
---|---|---|
Optional<T> | reduce(BinaryOperatorl<T> accumulator) | 2つの要素で処理を行うBinaryOperatorで結果と要素を使って処理を繰り返し行い、処理の結果をOptionalで返す。要素がないなどで結果がない場合はemptyなOptionalを返す |
T | reduce(T identifier, BinaryOperatorl<T> accumulator) | 最初の設定値として第1引数にTを設定。このTを最初の値としてBinaryOperatorで処理を繰り返し行い、その結果をTで返す。Streamに要素がない場合はTのidentifierの値が返る |
U | reduce(U identifier, BinaryFunction<U, ? super T,U> accumulator, BinaryOperator<U> combiner) | Streamの持つ型と結果として返す型が違う場合に使うreduceメソッド。第1引数を最初の値としてUを受け取り、BinaryFunctionでStreamの要素Tと受け取ったUを使って処理を行い、第1引数と同じUの型に変換して結果を返す。並列処理の場合は、第3引数のBinaryOperatorで分散されたBinaryFunctionの結果Uを受け取り、結果をまとめて生成する処理を行う。Streamに要素がない場合は第1引数の値が返る |
それではreduceメソッドを使ったサンプルを実行してみましょう。
public class ReductionSample1 { public static void main(String[] args) { List<BigDecimal> list = new ArrayList<BigDecimal>(); list.add(new BigDecimal("1")); list.add(new BigDecimal("2")); list.add(new BigDecimal("3")); list.add(new BigDecimal("4")); list.add(new BigDecimal("5")); List<String> strList = Arrays.asList("1", "2", "3", "4", "5"); List<BigDecimal> emptyList = new ArrayList<BigDecimal>(); List<String> emptyStrList = new ArrayList<String>(); System.out.println("----- 引数が1つの場合 -----"); // 直列処理の場合 Optional<BigDecimal> result1 = list.stream() .reduce((value1, value2) -> value1.add(value2)); System.out.println("[1] result1 = " + result1); // 並列処理の場合 Optional<BigDecimal> parallelResult1 = list.stream() .reduce((value1, value2) -> value1.add(value2)); System.out.println("[2] parallelResult1 = " + parallelResult1); // 空Listの場合 Optional<BigDecimal> emptyResult1 = emptyList.stream() .reduce((value1, value2) -> value1.add(value2)); System.out.println("[3] emptyResult1 = " + emptyResult1); // 空Listの場合 Optional<BigDecimal> emptyParallelResult1 = emptyList.parallelStream() .reduce((value1, value2) -> value1.add(value2)); System.out.println("[4] emptyParallelResult1 = " + emptyParallelResult1); System.out.println("----- 引数が2つの場合 -----"); // 直列処理の場合 BigDecimal result2 = list.stream() .reduce(BigDecimal.ZERO, (value1, value2) -> value1.add(value2)); System.out.println("[5] result2 = " + result2); // 並列処理の場合 BigDecimal parallelResult2 = list.parallelStream() .reduce(BigDecimal.ZERO, (value1, value2) -> value1.add(value2)); System.out.println("[6] parallelResult2 = " + parallelResult2); // 空Listの場合 BigDecimal emptyResult2 = emptyList.stream() .reduce(BigDecimal.ZERO, (value1, value2) -> value1.add(value2)); System.out.println("[7] emptyResult2 = " + emptyResult2); // 空Listの場合 BigDecimal emptyParallelResult2 = emptyList.parallelStream() .reduce(BigDecimal.ZERO, (value1, value2) -> value1.add(value2)); System.out.println("[8] emptyParallelResult2 = " + emptyParallelResult2); System.out.println("----- 引数が3つの場合 -----"); // 直列処理の場合 BigDecimal result3 = strList.stream() .reduce(BigDecimal.ZERO, (bdValue, strValue) -> bdValue.add(new BigDecimal(strValue)), (bdResult1, bdResult2) -> bdResult1.add(bdResult2)); System.out.println("[9] result3 = " + result3); // 並列処理の場合 BigDecimal parallelResult3 = strList.parallelStream() .reduce(BigDecimal.ZERO, (bdValue, strValue) -> bdValue.add(new BigDecimal(strValue)), (bdResult1, bdResult2) -> bdResult1.add(bdResult2)); System.out.println("[10] parallelResult3 = " + parallelResult3); // 空Listの場合 BigDecimal emptyResult3 = emptyStrList.stream() .reduce(BigDecimal.ZERO, (bdValue, strValue) -> bdValue.add(new BigDecimal(strValue)), (bdResult1, bdResult2) -> bdResult1.add(bdResult2)); System.out.println("[11] emptyResult3 = " + emptyResult3); // 空Listの場合 BigDecimal emptyParallelResult3 = emptyStrList.parallelStream() .reduce(BigDecimal.ZERO, (bdValue, strValue) -> bdValue.add(new BigDecimal(strValue)), (bdResult1, bdResult2) -> bdResult1.add(bdResult2)); System.out.println("[12] emptyParallelResult3 = " + emptyParallelResult3); } }
----- 引数が1つの場合 ----- [1] result1 = Optional[15] [2] parallelResult1 = Optional[15] [3] emptyResult1 = Optional.empty [4] emptyParallelResult1 = Optional.empty ----- 引数が2つの場合 ----- [5] result2 = 15 [6] parallelResult2 = 15 [7] emptyResult2 = 0 ----- 引数が3つの場合 ----- [8] emptyParallelResult2 = 0 [9] result3 = 15 [10] parallelResult3 = 15 [11] emptyResult3 = 0 [12] emptyParallelResult3 = 0
ここで注意するのが第1引数に値を設定するメソッドで、ここに設定する値は、この例だと数値の「0」など第2引数での処理に影響を受けないものにしないといけない点です。そうでないとStreamが並列処理の場合、分散されただけ第1引数に設定された値が第2引数の処理で使われ、直列処理と並列処理とで結果が違ってしまいます。
先ほどの引数を2つ受け取るものを、第1引数の値を100にして、受け取っている値を標準出力するようにした、次のサンプルを見てみましょう。
public class ReductionSample2 { public static void main(String[] args) { List<BigDecimal> list = new ArrayList<BigDecimal>(); list.add(new BigDecimal("1")); list.add(new BigDecimal("2")); list.add(new BigDecimal("3")); list.add(new BigDecimal("4")); list.add(new BigDecimal("5")); // 引数が2つの場合 System.out.println("--- 直列処理 ---"); BigDecimal result = list.stream() .reduce(new BigDecimal("100"), (value1, value2) -> { System.out.println("value1 = " + value1); System.out.println("value2 = " + value2); return value1.add(value2); }); System.out.println("result = " + result); System.out.println("--- 並列処理 ---"); BigDecimal parallelResult = list.parallelStream() .reduce(new BigDecimal("100"), (value1, value2) -> { System.out.println("value1 = " + value1); System.out.println("value2 = " + value2); return value1.add(value2); }); System.out.println("parallelResult = " + parallelResult); } }
この結果から分かるように、直列処理の場合は第1引数の値「(new BigDecimal("100"))」が一度だけ使われていますが、並列処理の場合は分散されただけ第1引数の値が使われているのが分かります。そのため直列処理と並列処理とで処理結果が変わってくることがあるので注意してください。
また、引数を3つ受け取るメソッドの第3引数は、分散された結果を集めて処理をするものになります。そのため第3引数は並列処理のときだけ呼ばれて、直列処理の場合は呼ばれません。
次のサンプルは先ほどの引数を3つ持つサンプルを第1引数の値を100に、第2引数と第3引数で値を受け取った際にその値を標準出力するように変更したものです。これを実行すると、並列処理を行った場合のみ、第3引数の処理が実行されていることが分かります。
public class ReductionSample3 { public static void main(String[] args) { List<String> list = Arrays.asList("1", "2", "3", "4", "5"); // 引数が3つの場合 System.out.println("--- 直列処理 ---"); BigDecimal result = list.stream() .reduce(new BigDecimal("100"), (bdValue, strValue) -> { System.out.println("第2引数"); System.out.println("bdValue = " + bdValue); System.out.println("strValue = " + strValue); return bdValue.add(new BigDecimal(strValue)); }, (bdResult1, bdResult2) -> { System.out.println("第3引数"); System.out.println("bdResult1 = " + bdResult1); System.out.println("bdResult2 = " + bdResult2); return bdResult1.add(bdResult2); }); System.out.println("結果"); System.out.println("result = " + result); System.out.println("--- 並列処理 ---"); BigDecimal parallelResult = list.parallelStream() .reduce(new BigDecimal("100"), (bdValue, strValue) -> { System.out.println("第2引数"); System.out.println("bdValue = " + bdValue); System.out.println("strValue = " + strValue); return bdValue.add(new BigDecimal(strValue)); }, (bdResult1, bdResult2) -> { System.out.println("第3引数"); System.out.println("bdResult2 = " + bdResult2); return bdResult1.add(bdResult2); }); System.out.println("結果"); System.out.println("parallelResult = " + parallelResult); } }
--- 直列処理 --- 第2引数 bdValue = 100 strValue = 1 第2引数 bdValue = 101 strValue = 2 第2引数 bdValue = 103 strValue = 3 第2引数 bdValue = 106 strValue = 4 第2引数 bdValue = 110 strValue = 5 結果 result = 115 --- 並列処理 --- 第2引数 bdValue = 100 strValue = 3 第2引数 bdValue = 100 strValue = 2 第2引数 bdValue = 100 strValue = 5 第2引数 bdValue = 100 strValue = 1 第3引数 bdResult1 = 101 bdResult2 = 102 第2引数 bdValue = 100 strValue = 4 第3引数 bdResult1 = 104 bdResult2 = 105 第3引数 bdResult1 = 103 bdResult2 = 209 第3引数 bdResult1 = 203 bdResult2 = 312 結果 parallelResult = 515
この結果より直列処理の場合は第3引数の処理は使われず、並列処理の場合は第3引数が使われているのが分かります。
また、第2引数は最初に第1引数の値とStreamの要素を使って処理をした際にだけ呼ばれ、それ以降の処理は第3引数の処理で結果を出していることが分かります。
Copyright © ITmedia, Inc. All Rights Reserved.