連載
» 2014年07月28日 18時00分 UPDATE

Java 8はラムダ式でここまで変わる(終):Stream APIの特殊なメソッドとメソッド参照/コンストラクター参照 (1/4)

本連載では、今までJavaの経験はあっても「ラムダ式は、まだ知らない」という人を対象にラムダ式について解説していきます。最終回は、Stream APIの特殊なメソッド3つと、ラムダ式と関係が深いメソッド参照やコンストラクター参照についてコード例を交えて解説。

[長谷川智之,株式会社ビーブレイクシステムズ]
「Java 8はラムダ式でここまで変わる」のインデックス

連載目次

 前回の「Stream APIの主なメソッドと処理結果のOptionalクラスの使い方」では、Stream APIの持つ主なメソッドについて見てきました。今回は前回の記事では紹介できなかったStream APIのメソッドと、ラムダ式と関係が深い「メソッド参照」「コンストラクター参照」について見ていきます。

 Stream APIには前回の記事で紹介したもの以外にもさまざまなものが用意されています。今回はStream APIの中でも使う際に注意をする必要があるメソッドを紹介します。

 またJava 8では、ラムダ式とともにメソッドの呼び出しやコンストラクターの呼び出しを簡易化したメソッド参照やコンストラクター参照の表記法を導入しました。これらはラムダ式と同様に「関数型インターフェース」を引数として持つメソッドで使われる表記法です。

指定した要素を作成してStreamを生成するgenerate/iterateメソッド

 今までの連載では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メソッド

 次に処理結果から集約処理を行う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つの要素で指定された処理を行い、その結果と次の要素を使ってまた同じ処理を行います。それを繰り返していき、最終的な結果を取得するメソッドです。

java8_5_01.jpg ※直列処理時のイメージです

 また、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引数の値が返る

3種類のreduceメソッドを使ったサンプル

 それでは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つのreduceメソッドの第1引数の値を100にして、受け取っている値を標準出力する

 先ほどの引数を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);
    }
}
java8_5_02.jpg 実行結果

 この結果から分かるように、直列処理の場合は第1引数の値「(new BigDecimal("100"))」が一度だけ使われていますが、並列処理の場合は分散されただけ第1引数の値が使われているのが分かります。そのため直列処理と並列処理とで処理結果が変わってくることがあるので注意してください。

 また、引数を3つ受け取るメソッドの第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引数の処理で結果を出していることが分かります。

       1|2|3|4 次のページへ

Copyright© 2017 ITmedia, Inc. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

Focus

- PR -

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。