ラムダ式とストリーム

Effective Java 第3版の個人的メモ

  1. 項目42 匿名クラスよりもラムダ式を選ぶ
  2. 項目43 ラムダよりもメソッド参照を選ぶ
  3. 項目44 標準の関数型インタフェースを使う
  4. 項目45 ストリームを注意して使う
  5. 項目46 ストリームで副作用のない関数を選ぶ
  6. 項目47 戻り値型としてStreamよりもCollectionを選ぶ
  7. 項目48 ストリームを並列化するときは注意を払う
  8. 参考URL

1. 項目42 匿名クラスよりもラムダ式を選ぶ

1.1. 結論

数行のコードで済む、もしくは、自分への参照が必要、関数型インターフェースの型しか使用しない場合は、ラムダ式を使いましょう。 それ以外の場合は、匿名クラスを使いましょう。

1.2. 匿名クラスでの実装

Anonymous.java

1
2
3
4
5
6
7
List<String> words = Arrays.asList("apple", "pen", "pineapple");
// 匿名クラスで書く。
Collections.sort(words, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});

1行で済んでおり、自分への参照は不要なので、ラムダ式の方が良いですね。

そもそも匿名クラスとは、、
以下のように記述される処理のことですね。

1
2
3
変数の型 変数 = new クラス名/インターフェース名( ) {
メソッドの処理を記述
};

1.3. ラムダ式での実装

Anonymous.java

1
2
3
List<String> words = Arrays.asList("apple", "pen", "pineapple");
// ラムダ式で書く。
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

数行で済んでおり、自分への参照はしておらず、Collections.sortの第2引数が関数型インターフェース(Comparator)なので、ラムダ式が向いていますね。

関数型インタフェースとは、、
関数型インターフェースの条件は、大雑把に言って、定義されている抽象メソッドが1つだけあるインターフェース。(staticメソッドやObjectクラスにあるデフォルトメソッドはカウントしない。)

Java8での関数型インタフェース一覧はOracle Java SE 8 (Uses of Class java.lang.FunctionalInterface)にあります。 3rd party製のライブラリで@FunctionalInterfaceが付いているインタフェースも、関数型インタフェースですね。

2. 項目43 ラムダよりもメソッド参照を選ぶ

2.1. 結論

  • メソッド参照の利点:ラムダ式よりも簡潔に書ける。
  • ただし、同クラスにあるメソッドを用いる場合は、ラムダ式の方が簡潔に書ける場合がある。

2.2. メソッド参照

メソッド参照とは、、
関数型インターフェース(抽象メソッドが1つだけ定義されているインターフェース)の変数にメソッドそのものを代入することが出来る。これを「メソッド参照」と呼ぶ。

MethodRefAndLambdaExpression.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var list = new ArrayList<Integer>();

for(var i = 0 ; i < 10 ; i++) {
    list.add(i);
}

// メソッド参照
list.forEach(System.out::print);

// ラムダ式
list.forEach(i -> System.out.print(i));

メソッド参照の方が、簡潔に記載できていますね。

2.3. メソッド参照について深掘り

メソッド参照には、下記5種類がある。

上記の例は、Systemクラスのstaticフィールドoutprintlnメソッドなので、静的メソッド参照だったんですね。

メソッド参照形式 コード 等価ラムダ
静的メソッド TypeName::method (args) -> TypeName.method(args)
非静的メソッド(インスタンス#1の場合) instance::method (args) -> instance.method(args)
非静的メソッド(インスタンスなし) TypeName::method (instance, args) -> instance.method(args)
コンストラクタ#2 TypeName::new (args) -> new TypeName(args)
配列コンストラクタ TypeName[]::new (int size) -> new TypeName[size]

#1 : instanceは、インスタンスへの参照を評価する任意の式です。例えば、getInstance()::methodthis::methodのように書けます。

#2 : TypeNameが非静的な内部クラスである場合、コンストラクタ参照は外部クラスインスタンスのスコープ内でのみ有効です。(つまり、外からコンストラクタが見れる場合使えますよ。という内容です。)

3. 項目44 標準の関数型インタフェースを使う

基本的には、標準の関数型インタフェースで事足りる。 しかし、特定の場合に限っては独自の関数インタフェースを定義する必要がある。

3.1. 標準の関数型インタフェース

3.2. 独自の関数型インタフェース

以下の場合、独自の関数型インタフェースを作成しましょう。

  • 広く使われ、説明的な名前によってメリットがある場合
  • 強く関係する契約がある場合(?)
  • 独自のデフォルトメソッドによるメリットがある場合

作成するときは、@FunctionalInterfaceを使いましょう。書き方に誤りがある場合、コンパイルエラーにしてくれます。

同じ名前のメソッドで、異なる関数型インタフェースを引数に持つのは、ユーザーにとって曖昧となるのでやめましょう。
例:ExecutorService.submit()は、異なる関数インタフェース「Runnable」と「Callable<T>」を引数に持っているのでよくありません。

3.3. 標準の関数型インタフェースについての余談

各標準の関数型インタフェースについて以下の観点でまとめておきます。

  • 従来の書き方
  • 匿名クラスでの書き方
  • lambda式を使った書き方
  • 呼び出し方

3.3.1. Function

  • 従来の書き方

    1
    2
    3
    
    R functionName(T arg) {
    return new R();
    }
  • 匿名クラスでの書き方

    1
    2
    3
    4
    5
    6
    
    Function<T, R> functionalInstance = new Function<T, R>() {
    @Override
    public R apply(T arg) {
        return new R();
    }
    };
  • lambda式を使った書き方

    1
    
    Function<T, R> functionalInstance = arg -> new R();
  • 呼び出し方

    1
    
    R returnValue = functionalInstance.apply((T) argument);

3.3.2. Consumer

  • 従来の書き方

    1
    2
    3
    
    void consumer(T arg) {
    something(arg);
    }
  • 匿名クラスでの書き方

    1
    2
    3
    4
    5
    6
    
    Consumer<T> consumer = new Consumer<T>() {
    @Override
    public void accept(T arg) {
        something(arg);
    }
    };
  • lambda式を使った書き方

    1
    
    Consumer<T> consumer = arg -> something(arg);
  • 呼び出し方

    1
    
    consumer.accept((T) argument)

3.3.3. Supplier

  • 従来の書き方

    1
    2
    3
    
    T supplier() {
    return new T();
    }
  • 匿名クラスでの書き方

    1
    2
    3
    4
    5
    6
    
    Supplier<T> supplier = new Supplier<T>() {
    @Override
    public T get() {
        return new T();
    }
    };
  • lambda式を使った書き方

    1
    
    Supplier<T> supplier = () -> new T();
  • 呼び出し方

    1
    
    supplier.get();

3.3.4. Predicate

偶奇判定を例に記載します。

  • 従来の書き方

    1
    2
    3
    
    boolean predicate(Integer i) {
      return i % 2 == 0 ? true : false;
    }
  • 匿名クラスでの書き方

    1
    2
    3
    4
    5
    6
    
    boolean predicate = new Predicate<T>(){
    @Override
    public boolean test(Integer i) {
        return i % 2 == 0;
    }
    };
  • lambda式を使った書き方

    1
    
    Predicate<Integer> checker = (i)-> { return i % 2 == 0; };
  • 呼び出し方

    1
    
    boolean result = checker.test(i);

3.3.5. UnaryOperator

Stringを強調させるメソッドを例に記載します。

  • 従来の書き方

    1
    2
    3
    
    public String apply(String t) {
    return "** " + t + " **";
    }
  • 匿名クラスでの書き方

    1
    2
    3
    4
    5
    6
    
    String str = new UnaryOperator<String>(){
    	@Override
    	public String apply(String t) {
    	    return "** " + t + " **";
    	}
    }
  • lambda式を使った書き方

    1
    
    UnaryOperator<String> operator = (String t) -> { return "**" + t + "**"; }
  • 呼び出し方

    1
    
    String newText = operator.apply(text);

3.3.6. BinaryOperator

足し算メソッドを例に記載します。

  • 従来の書き方

    1
    2
    3
    
    public Integer binaryOperator(Integer i1, Integer i2) {
    return i1 + i2;
    }
  • 匿名クラスでの書き方

    1
    2
    3
    4
    5
    6
    
    BinaryOperator<Integer> binaryOperator = new BinaryOperator<Integer>() {
    @Override
    public Integer apply(Integer i1, Integer i2) {
        return i1 + i2;
    }
    };
  • lambda式を使った書き方

    1
    2
    3
    
    BinaryOperator<Integer> binaryOperator = (Integer i1, Integer i2) -> {
    return i1 + i2;
    };
  • 呼び出し方

    1
    
    int actual = binaryOperator.apply(1, 2);

4. 項目45 ストリームを注意して使う

4.1. 結論

Stream処理により簡単になる場合は、主に以下である。
いつStreamを使うべきかという、かっちりとしたルールはない。

  • 単純な要素の変換
  • 要素のフィルタリング処理
  • 単純なオペレーションで要素を結びつける処理
  • 要素をCollectionに集約する処理
  • 特定の基準を満たす要素を探す処理

次のようなことになるのであれば、使わないでおきましょう。

  • Stream処理ばかりで、複雑になる場合
  • char型の値のStream処理
  • コードブロック内({...})で出来ることを捨ててまで、Stream処理にしない。

4.2. Stream処理ばかりで、複雑になる場合

4.2.1. 改善前

ファイル内の単語(行単位)を、アナグラムごとにまとめて、指定数以上含まれている場合は出力するプログラム。

アナグラム(anagram)とは、言葉遊びの一つで、単語または文の中の文字をいくつか入れ替えることによって、全く別の意味にさせる遊びである。Wikipedia

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var dictionary = new File("hoge.txt");
var minGroupSize = 2;
var groups = new HashMap<String, Set<String>>();

try (var words = Files.lines(dictionary)) {
    words.collect(Collectors.groupingBy(word -> word.chars().sorted()
            .collect(StringBuilder::new, (sb, c) -> sb.append((char) c), StringBuilder::append).toString()))
            .values().stream().filter(group -> group.size() >= minGroupSize)
            .map(group -> group.size() + ":" + group).forEach(System.out::println);
}

解説が必要なほど、複雑だと思うので、以下に記載します。。。(なんでもかんでもStream処理が良いという訳ではないということですね。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 行ごとの単語をソートする。(アナグラムとして捉えるため。)
word.chars().sorted()

// StringBuilderでソートした文字列を組み立てる。
.collect(StringBuilder::new, (sb, c) -> sb.append((char) c), StringBuilder::append).toString()))

// 指定数以上かどうかでフィルターをかける。
.values().stream().filter(group -> group.size() >= minGroupSize)

// 表示形式を指定して、出力する。
.map(group -> group.size() + ":" + group).forEach(System.out::println);

4.2.2. 改善後

ファイル内の単語(行単位)を、アナグラムごとにまとめて、指定数以上含まれている場合は出力するプログラム。

悪い例でややこしかった、文字列ソートの部分をalphabetizeメソッドに切り出しているので、わかりやすくなっていますね。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var dictionary = new File("hoge.txt");
var minGroupSize = 2;
var groups = new HashMap<String, Set<String>>();

try (var words = Files.lines(dictionary)) {
    words.collect(Collectors.groupingBy(word -> alphabetize(word)))
    .values().stream()
            .filter(group -> group.size() >= minGroupSize)
            .forEach(g -> System.out.println(g.size() + ":" + g));
}

private static String alphabetize(String s) {
    var a = s.toCharArray();
    Arrays.sort(a);
    return new String(a);
}

4.3. char型の値のStream処理

char型のStream処理は直感的でない動きをしうるので、原則としてchar型の値をStream処理で扱うべきでない。

1
2
3
4
5
6
7
8
 "Hello, world!".chars().forEach(System.out::print);
 // 実行結果
 // 72101108108111443211911111410810033

// (char)にキャストしてあげる。
 "Hello, world!".chars().forEach(x -> System.out.print((char) x));
 // 実行結果
 // "Hello, world!"が表示

4.4. コードブロック内({...})で出来ることを捨ててまで、Stream処理にしない。

関数型オブジェクトを使用したStream処理で出来ず、コードブロック内({...})で出来ること。

  • ローカル変数の使用(ラムダ式ではfinalでないと使用できない。)
  • 内部メソッドのreturn, ループ内でのbreak/continue, 例外スロー(ラムダ式では出来ない。)

5. 項目46 ストリームで副作用のない関数を選ぶ

5.1. 前提

Streamの最も重要なところは、純粋関数のみによる演算 であること。

純粋関数とは、副作用を持たない関数のことである。副作用とは、変数の再代入や入出力などによってプログラムの状態が変化することを指します。

副作用があることによるデメリット
・全体の流れが分かりづらくなる可能性がある。
・速度が落ち、並列処理時の問題が出る可能性がある。

5.2. 結論

副作用を持たないようにするため、ストリーム処理は以下のようにする。

  • forEachは、ストリームの演算の結果を示すためだけに利用する。
  • Collectorsを利用する。

5.3. forEachは、ストリームの演算の結果を示すためだけに利用する。

ファイルに含まれる単語の頻度を求めるプログラム。

以下は、ストリーム内で、外部の状態(freq変数)を変更しているため(副作用を持つため)、良くない。

1
2
3
4
5
6
7
// Uses the streams API but not the paradigm--Don't do this!
var freq = new HashMap<String, Long>();
try (Stream<String> words = new Scanner(file).tokens()) {
    words.forEach(word -> {
        freq.merge(word.toLowerCase(), 1L, Long::sum);
    });
}

以下が改善例。 ストリーム内で、外部の状態(freq変数)を変更しておらず、戻り値を代入している。

1
2
3
4
5
6
// Proper use of streams to initialize a frequency table
Map<String, Long> freq;
try (Stream<String> words = new Scanner(file).tokens()) {
    freq = words
        .collect(groupingBy(String::toLowerCase, counting()));
}

5.4. Collectorsを利用する。

ストリームパイプラインの可読性のために、Collectorsのメンバーはstaticインポートしておくべき。

5.4.1. toSet(), toCollection(), toList()

toSet(), toCollection(), toList()を利用した例。

1
2
3
4
List<String> topTen = freq.keySet().stream()
    .sorted(comparing(freq::get).reversed())
    .limit(10)
    .collect(toList());

5.4.2. toMap()

toSet(), toCollection(), toList()以外は、ストリームをMapにするためのものがほとんどである。

toMap()を使ったサンプルを載せる。

1
2
3
4
5
6
7
8
// e.ordinal()は、列挙定数の序数を返す。
Map<ENUM, Integer> map = Stream.of(ENUM.values())
    .collect(Collectors.toMap(
        // key
        e -> e, 
        // value
        e -> e.ordinal()
    ));
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
HashMap<ENUM, Integer> map= Stream.of(ENUM.values())
    .collect(Collectors.toMap(
        // key
        e -> e
        // value
        ,e -> e.ordinal()
        // 同一キーの値が複数あった場合、最初の要素を優先する
        // 最後の要素を優先する場合は`(e1, e2) -> e2`とする
        ,(e1, e2) -> e1
        // HashMap型を指定する。
        ,HashMap::new
    ));

5.4.3. groupingBy()

groupingBy()は、classifierをもとに要素をカテゴリー分けしたMapを生成する。

classifierには、以下のサンプルのt -> t.length() や 「項目45 ストリームを注意して使う」で紹介したalphabetize(word)が該当する。

1
2
3
4
5
Stream<String> s = Stream.of("a", "bar", "c", "foo", "zzz");
Map<Integer, List<String>> m = s.collect(Collectors.groupingBy(t -> t.length()));
System.out.println(m);
// 実行結果:
// {1=[a, c], 3=[bar, foo, zzz]}

groupingBy()の2つ引数を取るシンプルな例としては以下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import static java.util.stream.Collectors.*;
.
.
.
Stream<String> words = Stream.of("A", "Bar", "C", "fOO", "zZz", "A");
Map<String, Long> freq = words
        .collect(groupingBy(String::toLowerCase, counting()));
System.out.println(freq);
// 実行結果
// {a=2, bar=1, c=1, foo=1, zzz=1}

5.4.4. その他のメソッド

downstream collectorとしての利用に特化した以下のメソッドは、collect(counting())のように利用してはいけない。 Streamに同様の機能があるため。

  • counting()
    • 要素数を取得する。
  • summing*()
    • 入力値を型変換して、合算する。
  • averaging*()
    • 入力値を型変換して、平均値を算出する。 空ストリームの場合は0を返す。
  • summarizing*()
    • 入力値を型変換して、各種集計値を算出する。
  • reducing()
    • 値を集約する。
  • filtering()
    • 条件を満たす値だけ他のコレクターに渡す。
  • mapping()
    • 値を変換して他のコレクターを呼び出す。
  • flatMapping()
    • 値をStreamに変換して他のコレクターを呼び出す。
  • collectingAndThen()
    • 他のコレクターを呼び出した結果を変換する。
  • minBy()
    • 最小値を取得する。
  • maxBy()
    • 最大値を取得する。
  • joining()
    • 結合してStringにする。

CollectorsStreamの利用方法については、こちらのブログ(Java Collector, Java Stream)が入門として(大まかに把握するのに)良さそうである。

6. 項目47 戻り値型としてStreamよりもCollectionを選ぶ

6.1. 結論

連続した要素の戻り値型として以下のものがあげられる。

大抵の場合は、最適な戻り値型は、Collection適切なCollectionのサブタイプである。
理由は、Collectioniterator()stream()を持っており、イテレーション処理でもストリーム処理でも対応できるからである。

もしイテレーション処理やストリーム処理を使う場合は、使用するユーザーによっては、ストリームからイテレートできるように変換するコードを作る必要になることを念頭に置いておきましょう。

6.2. Collectionを利用する場合

6.2.1. 返却する連続要素が小さい場合

Collectionを実装したもの(ArrayListHashSetなど)を返しましょう。

6.2.2. 返却する連続要素が大きい場合

返却する連続要素が大きい場合の例として、与えられた集合のべき集合を返す場合を考える。
この場合、メモリに蓄えるべきではないです。下の例のように、組み合わせ数が膨大になっていくからである。(組み合わせ数 : 2^(要素数))

{a,b,c}のべき集合の例: {{}, {a}, {b}, {c}, {a, b}, {a, c}, {b, c}, {a, b, c}}

このような場合は、GuavaのSets#powerSet()などを利用しましょう。(もしThirdParty製のライブラリが無ければ、独自実装しましょう。)
中のロジックは、以下のようなSubsetを利用したビット演算である。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Set  = [a,b,c]
power_set_size = pow(2, 3) = 8
Run for binary counter = 000 to 111

Value of Counter            Subset
    000                    -> Empty set
    001                    -> a
    010                    -> b
    011                    -> ab
    100                    -> c
    101                    -> ac
    110                    -> bc
    111                    -> abc

サンプル実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Java program for power set 
import java .io.*; 
  
public class GFG { 
      
    static void printPowerSet(char []set, 
                            int set_size) 
    { 
          
        /*set_size of power set of a set 
        with set_size n is (2**n -1)*/
        long pow_set_size =  
            (long)Math.pow(2, set_size); 
        int counter, j; 
      
        /*Run from counter 000..0 to 
        111..1*/
        for(counter = 0; counter <  
                pow_set_size; counter++) 
        { 
            for(j = 0; j < set_size; j++) 
            { 
                /* Check if jth bit in the  
                counter is set If set then  
                print jth element from set */
                if((counter & (1 << j)) > 0) 
                    System.out.print(set[j]); 
            } 
              
            System.out.println(); 
        } 
    } 
      
    // Driver program to test printPowerSet 
    public static void main (String[] args) 
    { 
        char []set = {'a', 'b', 'c'}; 
        printPowerSet(set, 3); 
    } 
} 
  
// This code is contributed by anuj_67. 

メモ:集合が31以上の要素を持っている場合、GuavaのSets#powerSet()は例外をスローする。べき集合の大きさが2^31以上となり、int size()の最大値(2^31-1)的に対応できないからである。

7. 項目48 ストリームを並列化するときは注意を払う

7.1. 結論

ストリームの並列化により、性能が良くなるのは以下です。

  • サブレンジへの分割が容易であること
  • 逐次処理される時の参照の局所性
  • 終端処理がリダクション処理や短絡評価である場合

また、指標として、(ストリームの要素の数) * (1要素に実行されるコード行数) > 100000 であれば、並列化により性能が向上するかも。

以下の場合、並列化をしても性能は良くならないでしょう。

  • Stream.iterateを使っている
  • 中間操作limitを使っている

並列化により、間違った結果や予想できない動作を起こす可能性があるので気をつけましょう。例えばforEach()forEachOrded()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// データの準備
List<String> list = Arrays.asList(new String[]{"あ", "い", "う", "え", "お"});

// forEach()の場合
Stream<String> parallelStream1 = list.parallelStream();
parallelStream1.forEach(value -> System.out.println("forEach: " + value));

// forEachOrdered()の場合
Stream<String> parallelStream2 = list.parallelStream();
parallelStream2.forEachOrdered(value -> System.out.println("forEachOrdered: " + value));

// 実行結果
/* forEach: う
   forEach: あ
   forEach: え
   forEach: い
   forEach: お
   forEachOrdered: あ
   forEachOrdered: い
   forEachOrdered: う
   forEachOrdered: え
   forEachOrdered: お
  */

7.2. 「サブレンジへの分割が容易であること」と「逐次処理される時の参照の局所性」

ArrayListHashMapHashSetConcurrentHashMap、配列、intの範囲をもったもの、longの範囲を持ったもののストリームであれば、以下。

  • サブレンジへの分割ができる
  • 逐次処理される時の参照が局所化される

7.3. 「終端処理がリダクション処理や短絡評価である場合」

終端処理で、以下をやっていると並列による性能向上はあまり得られない。

  • パイプライン全体に対して大量の処理をする
  • 終端処理が内部的に逐次処理を行うものである。

7.3.1. 終端処理がリダクション処理

終端処理が、minmaxcountsumといったリダクション処理であれば、効果を得られる。

リダクション処理とは、ストリーム内のすべての要素を累積関数を使って一つにまとめた結果を返す操作。

7.3.2. 終端処理が短絡評価

終端処理が、anyMatchallMatchnoneMatchといった短絡評価は並列化の効果を得やすい。

短絡評価とは、&&||といった論理演算子において、左辺を評価した時点で、式全体の真偽値が決定し右辺を評価する必要がない場合、右辺を評価しないという機能のこと。(要は無駄な処理をしないで、真偽値を決めれる機能のこと)

7.4. 並列処理による性能向上

並列化が効率的に行える、素数計数関数のプログラムで確認する。
(以下の例では、.parallel()を付けたことにより、36秒 ⇨ 24秒 に短縮できています。)

素数計数関数とは、正の実数にそれ以下の素数の個数を対応させる関数のことであり、π(x) で表す。

7.4.1. 並列化前

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import java.math.BigInteger;
import java.util.stream.LongStream;

public class ParallelTest1 {
    // Prime-counting stream pipeline - benefits from parallelization
    static long pi(long n) {
        return LongStream.rangeClosed(2, n).mapToObj(BigInteger::valueOf).filter(i -> i.isProbablePrime(50)).count();
    }

    public static void main(String[] args) {
    	System.out.println(new Date());
        System.out.println(pi(10000000));
    	System.out.println(new Date());
    	// 実行結果
    	/*
	Tue Oct 22 21:47:03 JST 2019
	64579
	Tue Oct 22 21:47:39 JST 2019
	*/
    }
}

7.4.2. 並列化後

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.math.BigInteger;
import java.util.Date;
import java.util.stream.LongStream;

public class ParallelTest2 {
	// Prime-counting stream pipeline - benefits from parallelization
	static long pi(long n) {
		return LongStream.rangeClosed(2, n).parallel().mapToObj(BigInteger::valueOf).filter(i -> i.isProbablePrime(50)).count();
	}

	public static void main(String[] args) {
		System.out.println(new Date());
		System.out.println(pi(10000000));
		System.out.println(new Date());
		// 実行結果
		/*
		 Tue Oct 22 21:54:06 JST 2019
		 664579
		 Tue Oct 22 21:54:30 JST 2019
		 */
	}
}

8. 参考URL