M.Hiroi's Home Page

Java Programming

続・お気楽 Java プログラミング入門

[ PrevPage | Java | NextPage ]

Optioal<T>

Optioal<T> は Java 8 から導入されたクラスです。簡単に説明すると、値をひとつ保持しているか、もしくは値が無いことを表すクラスです。他のプログラミング言語では、SML/NJ や OCaml などの Option 型、Haskell の Maybe、Scala の Option クラスに相当する機能です。

たとえば、データを探索するメソッドで、データが見つからなかったときに null を返すよりも、Optional<T> を使ったほうが安全なプログラムを作ることができます。また、ジェネリクスのほかにも int, long, double 専用の Optional も用意されています。

●Optional<T> の生成

Optional<T> のインスタンスはスタティックメソッドで生成します。

  1. Optional.of(T e)
  2. Optional.ofNullable(T e)
  3. Optional.empty()

1 は引数 e を格納したインスタンスを生成します。null 以外の値であることが確実な場合に使います。2 は引数 e が null の場合は値がないことを表すインスタンス (empty) を、そうでなければ値を格納したインスタンスを生成します。3 は文字通りの意味で、empty を生成します。

簡単な例を示しましょう。

jshell> Optional.of(123)
$1 ==> Optional[123]

jshell> Optional.of(1.2345)
$2 ==> Optional[1.2345]

jshell> Optional.ofNullable(456)
$3 ==> Optional[456]

jshell> Optional.ofNullable(null)
$4 ==> Optional.empty

jshell> Optional.empty()
$5 ==> Optional.empty

●Optional<T> のメソッド

値の有無はメソッド isPresent() で調べることができます。値は get() で取得することができますが、empty に get() を適用すると NoSuchElementException という例外が送出されます。これでは null と変わらないので不便ですね。値を取得する、もしくはメソッドに渡したい場合は、次のメソッドを使うと便利です。

簡単な例を示しましょう。

jshell> var a = Optional.of(123)
a ==> Optional[123]

jshell> a.orElse(0)
$7 ==> 123

jshell> a.ifPresent(System.out::println)
123

jshell> var b = Optional.empty()
b ==> Optional.empty

jshell> b.orElse(0)
$11 ==> 0

jshell> b.ifPresent(System.out::println)

最後の ifPresent() は Optional に値がないので、println() は実行されません。

●Optional<T> の高階関数

Optional のデータを処理する場合、その結果を Optional に包んで返したい場合があります。このような場合、いちいちデータを取り出すのは面倒なので、Optional のままデータを処理できると便利です。Optional<T> には、このような処理に適した高階関数が用意されています。

flatMap() の使い方がちょっと難しいかもしれません。たとえば、map() の引数に map() を渡すことを考えてみましょう。この場合、引数の map() の返り値は Optional になり、それを Optional に包んで返すので、返り値のデータ型は Optional<Optional<T>> になってしまいます。つまり Optional が二重になってしまうのです。このような場合、flatMap() を使うと便利です。

Lisp などの関数型言語や論理型言語の Prolog など、リストを操作するプログラミング言語では、リストの中にリストを格納することができます。二重になったリストをフラットなリストに変換する操作を「平坦化 (flattening)」といいます。このような処理を行う関数は、多くの処理系で flatten という名前で定義されています。

簡単な例を示しましょう。

jshell> var a = Optional.of(1234)
a ==> Optional[1234]

jshell> a.filter(x -> x % 2 == 0)
$2 ==> Optional[1234]

jshell> a.filter(x -> x % 2 != 0)
$3 ==> Optional.empty

jshell> a.map(x -> x * x)
$4 ==> Optional[1522756]

jshell> var b = a.map(x -> x * x)
b ==> Optional[1522756]

jshell> a.flatMap(x -> b.map(y -> x + y))
$6 ==> Optional[1523990]

一番最後の例は、a または b が empty の場合でも結果は empty になります。このように、高階関数を使うと Optional のまま処理することができます。また、これらのメソッドをつないで処理した場合、Optional ならば途中で値が empty になったとしても、最終結果は empty になります。

つまり、Optional のメソッドチェインでは、途中で empty をチェックする必要がないのです。その分だけプログラムを簡単に記述することができます。Haskell の Maybe や Either のように、簡単なエラー処理に Optional を使うことができるかもしれません。興味のある方はいろいろ試してみてください。


初版 2016 年 12 月 4 日
改訂 2021 年 2 月 21 日

Stream

Stream は Java 8 から導入されたパッケージです。Stream は配列やコレクションクラスを「ストリーム (stream)」として操作します。Stream にはマッピング、フィルター、畳み込みなど便利な高階関数が多数用意されていて、マルチコアによる「並列処理」にも対応しています。今回は Stream の基本的な使い方を簡単に説明します。

●Stream の構成

Stream の操作は、Stream の生成、中間操作、終端操作の 3 つに分けることができます。

Stream の生成 → 中間操作A → ... → 中間操作Z → 終端操作 

中間操作は Stream を返すメソッドのことです。中間操作のメソッドはドット ( . ) で複数のメソッドをつないで処理することができます。終端操作は Stream を返さないメソッドのことで、Stream からデータを取得して処理を行い、その結果を返します。Stream は遅延ストリームなので、終端操作を実行したときに必要な数だけデータが生成され、それが Stream の中を流れて処理されます。もちろん、Stream は無限ストリームでも取り扱うことができます。

なお、終端操作を行った Stream をもう一度使用すると、実行時にエラーが送出されます。同じ Stream を再度使いたい場合は、もう一度 Stream を生成する必要があります。関数型言語の場合、遅延ストリームは immutable に実装されますが、Java の Stream は mutable です。ご注意くださいませ。

●Stream の生成

Stream を表すデータ型 (インターフェース) には次の 4 種類があります。

  1. Stream<T>
  2. IntStream
  3. LongStream
  4. DoubleStream

2, 3, 4 は基本的なデータ型 (int, long, double) を扱うストリームです。本稿ではプリミティブ型ストリームと呼ぶことにします。

コレクションクラスから Stream を生成するにはメソッド stream() を使います。配列から Stream を生成するにはスタティックメソッド Arrays.stream() を使います。このほかにも、Stream にはスタティックメソッド of(), builder(), iterate(), generate() などや、IntStream と LongStream 専用の range(), rangeClosed() が用意されています。

それでは簡単な例題として、整数列を生成するプログラムを作ってみましょう。range(n, m) は n 以上 m 未満の整数列を、rangeClosed(n, m) は n 以上 m 以下の整数列を生成します。次のリストを見てください。

jshell> /imports
|    ・・・省略・・・
|    import java.util.stream.*

jshell> var xs = IntStream.range(0, 10)
xs ==> java.util.stream.IntPipeline$Head@42110406

jshell> xs.forEach(System.out::println)
0
1
2
3
4
5
6
7
8
9

jshell> var ys = IntStream.iterate(1, y -> y + 2)
ys ==> java.util.stream.IntPipeline$Head@2cdf8d8a

jshell> ys.limit(10).forEach(System.out::println)
1
3
5
7
9
11
13
15
17
19

JShell はあらかじめ java.util.stream をインポートしているので簡単に試してみることができます。Stream の forEach() は終端操作のメソッドで、ストリームから要素を取り出し、それを引数のメソッドやラムダ式に渡して実行します。整数列は iterate() でも生成することができます。第 1 引数が初期値、第 2 引数のメソッドで前項から次項を生成します。iterate() は無限ストリームになるので、メソッド limit() で生成する要素の個数の上限値を設定します。

●マッピング

メソッド map() はストリームの要素に引数のメソッドを適用して、その結果をストリームに格納して返します。このような操作を「マッピング (写像)」といいます。基本的なメソッドが map() で、平坦化を行うマップ関数 flatMap() も用意されています。

簡単な例を示しましょう。次のリストを見てください。

jshell> var a = new Integer[] {1,2,3,4,5,6,7,8,9}
a ==> Integer[9] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }

jshell> var xs = Arrays.stream(a)
xs ==> java.util.stream.ReferencePipeline$Head@6659c656

jshell> xs.map(x -> x * x).forEach(x -> System.out.print(x + " "))
1 4 9 16 25 36 49 64 81
jshell> var ys = Arrays.stream(a)
ys ==> java.util.stream.ReferencePipeline$Head@e2d56bf

jshell> ys.map(x -> x * 1.5).forEach(x -> System.out.print(x + " "))
1.5 3.0 4.5 6.0 7.5 9.0 10.5 12.0 13.5

map() には関数型インターフェース Function に適合するメソッドを渡してください。Funciton のメソッドは、入力の型が T で出力の型が R なので、map() によって Stream<T> が Stream<R> に変化することがあります。上記リストでは、Stream xs は Integer のままですが、Stream ys は map() によって Integer から Double に変換されています。

整数を文字列に変換することもできます。たとえば、FizzBuzz のプログラムは Stream を使うと次のようになります。

jshell> String changeToFizzBuzz(int x) {
   ...>     if (x % 15 == 0) return "FizzBuzz";
   ...>     else if (x % 3 == 0) return "Fizz";
   ...>     else if (x % 5 == 0) return "Buzz";
   ...>     else return String.valueOf(x);
   ...>   }
|  次を作成しました: メソッド changeToFizzBuzz(int)

jshell> var zs = Stream.iterate(1, n -> n + 1)
zs ==> java.util.stream.ReferencePipeline$Head@4aa8f0b4

jshell> zs.map(x -> changeToFizzBuzz(x)).limit(100).forEach(x -> System.out.print(x + " "))
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 16 17 Fizz 19 Buzz Fizz 
22 23 Fizz Buzz 26 Fizz 28 29 FizzBuzz 31 32 Fizz 34 Buzz Fizz 37 38 Fizz Buzz
41 Fizz 43 44 FizzBuzz 46 47 Fizz 49 Buzz Fizz 52 53 Fizz Buzz 56 Fizz 58 59 FizzBuzz 
61 62 Fizz 64 Buzz Fizz 67 68 Fizz Buzz 71 Fizz 73 74 FizzBuzz 76 77 Fizz 79 Buzz 
Fizz 82 83 Fizz Buzz 86 Fizz 88 89 FizzBuzz 91 92 Fizz 94 Buzz Fizz 97 98 Fizz Buzz

スタティックメソッド changeToFizzBuzz() で整数を文字列に変換します。あとは、changeToFizzBuzz() を呼び出すラムダ式を map() に渡せばいいわけです。

なお、プリミティブ型ストリームにも専用の map() が用意されています。また、他のストリームに変換するメソッド mapToObj(), mapToInt(), mapToLong(), mapToDouble() などもあります。詳細は Java の マニュアル をお読みください。

●フィルター

メソッド filter() はストリームの要素に引数のメソッドを適用し、メソッドが真を返す要素をストリームに格納して返します。関数型言語の世界では、真または偽を返す関数のことを「述語 (predicate)」といいます。Java では関数型インターフェース Predicate<T> が述語を表します。宣言されているメソッドは boolean test(T x) です。

簡単な例を示しましょう。次のリストを見てください。

jshell> var a = new Integer[] {5, 6, 4, 7, 3, 8, 2, 9, 1, 0}
a ==> Integer[10] { 5, 6, 4, 7, 3, 8, 2, 9, 1, 0 }

jshell> var xs = Arrays.stream(a)
xs ==> java.util.stream.ReferencePipeline$Head@421faab1

jshell> xs.filter(x -> x % 2 == 0).forEach(x -> System.out.print(x + " "))
6 4 8 2 0
jshell> var ys = Arrays.stream(a)
ys ==> java.util.stream.ReferencePipeline$Head@799f7e29

jshell> ys.filter(x -> x < 5).forEach(x -> System.out.print(x + " "))
4 3 2 1 0

filter() にラムダ式 x -> x % 2 == 0 を渡すと、整数列から偶数の項を取り出すことができます。x -> x < 5 を渡すと 5 未満の項を取り出すことができます。なお、プリミティブ型ストリームにも専用の filter() が用意されています。

●畳み込み

2 つの引数を取る関数 f と List を引数に受け取る関数 reduce を考えます。reduce はリストの各要素に対して関数 f を下図のように適用します。

(1) [a1, a2, a3, a4, a5]
    => f( f( f( f( a1, a2 ), a3 ), a4 ), a5 )

(2) [a1, a2, a3, a4, a5]
    => f( a1, f( a2, f( a3, f( a4, a5 ) ) ) )


        図 : 関数 reduce の動作 (A)

関数 f を適用する順番で 2 通りの方法があります。図 (1) はリストの先頭から f を適用し、図 (2) はリストの後ろから f を適用します。たとえば、関数 f が単純な加算関数とすると、reduce の結果はどちらの場合もリストの要素の和になります。

f(x, y) = x + y の場合
reduce => a1 + a2 + a3 + a4 + a5

このように、reduce は List のすべての要素を関数 f を用いて結合します。このような操作を「縮約」とか「畳み込み」といいます。また、reduce の引数に初期値 g を指定することがあります。この場合、reduce は下図に示す動作になります。

(1) (a1, a2, a3, a4, a5)
    => f( f( f( f( f( g, a1 ), a2 ), a3 ), a4 ), a5 )

(2) (a1, a2, a3, a4, a5)
    => f( a1, f( a2, f( a3, f( a4, f( a5, g ) ) ) ) )


        図 : reduce() の動作 (B)

Stream の reduce() にはいくつか種類があるのですが、ここでは図 (A, B) (1) の動作を行う reduce() を取り上げます。

  1. Optional<T> reduce(BinaryOperator<T> accumulator)
  2. T reduce(T g, BinaryOperator<T> accumulator)

BinaryOperator は BiFunction を継承しています。BiFunction は引数の型や返り値の型が異なってもよいのですが、BinaryOperator は引数と返り値の型は同じ型 T でなければいけません。ストリームが空の場合、1 は Optional.empty を返します。2 は初期値 g を返します。

なお、初期値 g は並列処理の関係で f(g, x) == x を満たす値、つまり「単位元」を指定してください。たとえば、足し算であれば 0 を、掛け算であれば 1 を指定します。そうしないと、シーケンシャルな処理 (直列処理) と並列処理で結果が異なることがあります。これは並列処理のところで説明します。

簡単な例を示しましょう。

jshell> var a = new Integer[] {5, 6, 4, 7, 3, 8, 2, 9, 1, 10}
a ==> Integer[10] { 5, 6, 4, 7, 3, 8, 2, 9, 1, 10 }

jshell> var xs = Arrays.stream(a)
xs ==> java.util.stream.ReferencePipeline$Head@7aec35a

jshell> xs.reduce(0, (x, y) -> x + y)
$8 ==> 55

jshell> var ys = Arrays.stream(a)
ys ==> java.util.stream.ReferencePipeline$Head@579bb367

jshell> ys.reduce(1, (x, y) -> x * y)
$10 ==> 3628800

直列処理の場合、ラムダ式の第 1 引数には計算途中の値が格納されます。これを「累積変数」といいます。Stream の要素は第 2 引数に渡されます。初期値に 0 を指定して、ラムダ式の 2 つの引数を足し算すれば、Stream の合計値を求めることができます。初期値に 1 を指定して掛け算すれば、全ての要素を乗算した値を求めることができます。

●collect メソッド

collect() は終端操作のメソッドで、Stream の要素を集めて処理し、その結果を返します。collect() は 2 種類ありますが、インターフェース Collector を受け取るメソッドが便利です。クラス Collectors には、Collector を生成する便利なスタティックメソッドが多数用意されているので、簡単に collect() を使うことができます。

簡単な例を示しましょう。次のリストを見てください。

リスト : collect() の使用例

import java.util.Arrays;
import java.util.List;
import java.util.Comparator;
import java.util.Map;
import java.util.stream.Collectors;

enum Group { A, B, C, D }

class Person {
  int id;
  String name;
  double height;
  Group  gr;

  Person(int id, String name, double height, Group gr) {
    this.id = id;
    this.name = name;
    this.height = height;
    this.gr = gr;
  }

  int getId() { return id; }
  String getName() { return name; }
  double getHeight() { return height; }
  Group getGroup() { return gr; }

  public String toString() {
    return "(" + String.valueOf(id) + ","
      + name + "," + String.valueOf(height) + "," + gr + ")";
  }
}
  
public class sample150 {
  public static void main(String[] args) {
    List<Group> gs = Arrays.asList(Group.A, Group.B, Group.C, Group.D);
    List<Person> ps = Arrays.asList(
      new Person(1, "Ada",     148.7, Group.A),
      new Person(2, "Alice",   149.5, Group.B),
      new Person(3, "Carey",   133.7, Group.C),
      new Person(4, "Ellen",   157.9, Group.D),
      new Person(5, "Hanna",   154.2, Group.A),
      new Person(6, "Janet",   147.8, Group.B),
      new Person(7, "Linda",   154.6, Group.C),
      new Person(8, "Maria",   159.1, Group.D),
      new Person(9, "Miranda", 148.2, Group.A),
      new Person(10,"Sara",    153.1, Group.B),
      new Person(11,"Tracy",   138.2, Group.C),
      new Person(12,"Violet",  138.7, Group.D));

    System.out.println(ps.stream().map(x -> x.getName()).collect(Collectors.toList()));
    for (Group g: gs)
      System.out.println(ps.stream().filter(x -> x.getGroup() == g).collect(Collectors.toList()));
    
    System.out.println(ps.stream().collect(Collectors.summingDouble(x -> x.getHeight())));
    System.out.println(ps.stream().collect(Collectors.averagingDouble(x -> x.getHeight())));
    System.out.println(ps.stream().mapToDouble(x -> x.getHeight()).sum());
    System.out.println(ps.stream().mapToDouble(x -> x.getHeight()).average());

    Comparator<Person> compHeight = (x, y) -> {
      if (x.getHeight() < y.getHeight()) return -1;
      if (x.getHeight() > y.getHeight()) return 1;
      return 0;
    };

    System.out.println(ps.stream().collect(Collectors.maxBy(compHeight)));
    System.out.println(ps.stream().collect(Collectors.minBy(compHeight)));
    System.out.println(ps.stream().max(compHeight));
    System.out.println(ps.stream().min(compHeight));

    for (Group g: gs) {
      System.out.println(ps.stream().filter(x -> x.getGroup() == g).collect(Collectors.maxBy(compHeight)));
      System.out.println(ps.stream().filter(x -> x.getGroup() == g).collect(Collectors.minBy(compHeight)));
    }

    Map<Group, List<Person>> groups =
      ps.stream().collect(Collectors.groupingBy(x -> x.getGroup()));
    System.out.println(groups);
    for (Group g: gs)
      System.out.println(groups.get(g).stream().collect(Collectors.averagingDouble(x -> x.getHeight())));
  }
}

Person は個人の情報 (id, name, height, group) を表すクラスです。Arrays.asList() で Person を List に格納して変数 ps にセットします。変数 gs は Group を格納した List です。

toList() は Stream の要素を List に格納して返します。filter() を使うと条件に適合した要素を List に格納することができます。summingDouble() は要素を double に変換するメソッドを受け取り、要素の合計を計算します。averagingDouble() は平均値を計算します。これらの処理は、mapToDouble() で DoubleStream に変換して、メソッド sum() や average() を使っても求めることができます。

身長 (height) を比較するメソッドを変数 compHeight にセットします。このメソッドを maxBy(), minBy() に渡すと最大値と最小値を求めることができます。Stream のメソッド max(), min() でも同じことができます。filter() でグループに分けると、グループごとの最大値と最小値を求めることができます。

グループ分けは groupingBy() でも行うことができます。groupingBy() にはキーを指定するメソッドを渡してください。この場合、返り値は Map<K, List<T>> になります。Map はキーとそれに対応する値を組にしたコレクションのインターフェースです。実装にはハッシュ法を使った HashMap や二分木 (赤黒木) を使った TreeMap などが用意されています。

この場合、Map のキーが Group で、値には Group に属する要素を格納した List がセットされます。キーに対応する値はメソッド get() で取得することができます。値は List なので、stream() で Stream に変換して averagingDouble() を適用すれば、Group ごとの平均身長を求めることができます。

実行結果は次のようになりました。

$ javac sample150.java
$ java sample150
[Ada, Alice, Carey, Ellen, Hanna, Janet, Linda, Maria, Miranda, Sara, Tracy, Violet]
[(1,Ada,148.7,A), (5,Hanna,154.2,A), (9,Miranda,148.2,A)]
[(2,Alice,149.5,B), (6,Janet,147.8,B), (10,Sara,153.1,B)]
[(3,Carey,133.7,C), (7,Linda,154.6,C), (11,Tracy,138.2,C)]
[(4,Ellen,157.9,D), (8,Maria,159.1,D), (12,Violet,138.7,D)]
1783.6999999999998
148.64166666666665
1783.6999999999998
OptionalDouble[148.64166666666665]
Optional[(8,Maria,159.1,D)]
Optional[(3,Carey,133.7,C)]
Optional[(8,Maria,159.1,D)]
Optional[(3,Carey,133.7,C)]
Optional[(5,Hanna,154.2,A)]
Optional[(9,Miranda,148.2,A)]
Optional[(10,Sara,153.1,B)]
Optional[(6,Janet,147.8,B)]
Optional[(7,Linda,154.6,C)]
Optional[(3,Carey,133.7,C)]
Optional[(8,Maria,159.1,D)]
Optional[(12,Violet,138.7,D)]
{B=[(2,Alice,149.5,B), (6,Janet,147.8,B), (10,Sara,153.1,B)],
 C=[(3,Carey,133.7,C), (7,Linda,154.6,C), (11,Tracy,138.2,C)],
 A=[(1,Ada,148.7,A), (5,Hanna,154.2,A), (9,Miranda,148.2,A)],
 D=[(4,Ellen,157.9,D), (8,Maria,159.1,D), (12,Violet,138.7,D)]}
150.36666666666665
150.13333333333333
142.16666666666666
151.9

このほかにも、Collectors には便利なスタティックメソッドが用意されています。詳細は Java の マニュアル をお読みください。

●並列処理

Stream はマルチコアによる並列処理に対応しています。コレクションクラスであれば、stream() を parallelStream() に書き換える、もしくは Stream の中間操作 parallel() を使用します。

簡単な例を示しましょう。次のリストを見てください。

リスト : 並列処理

import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class sample151 {
  static int fibo(int n) {
    if (n < 2)
      return n;
    else
      return fibo(n - 2) + fibo(n - 1);
  }
  
  public static void main(String[] args) {
    List<Integer> xs = Arrays.asList(36, 35, 34, 33, 32, 31, 30, 29);
    List<Integer> ys = xs.stream().map(x -> fibo(x)).collect(Collectors.toList());
    Optional<Integer> r1 = xs.stream().map(x -> fibo(x)).findAny();
    Optional<Integer> r2 = xs.parallelStream().map(x -> fibo(x)).findAny();
    Optional<Integer> r3 = xs.parallelStream().map(x -> fibo(x)).findFirst();
    System.out.println(ys);
    System.out.println(r1);
    System.out.println(r2);
    System.out.println(r3);
  }
}

29 から 36 までのフィボナッチ数を計算します。スタティックメソッド fibo() は二重再帰になっているので、数が増えると時間がかかるようになります。findFirst() と findAny() は Stream から要素をひとつ取り出す終端操作のメソッドです。findFirst() は Stream の先頭要素を取り出し、findAny() は Stream の要素のどれかひとつを取り出します。

実行結果は次のようになります。

$ javac sample151.java
$ java sample151
[14930352, 9227465, 5702887, 3524578, 2178309, 1346269, 832040, 514229]
Optional[14930352]
Optional[514229]
Optional[14930352]

並列処理の場合、fibo() は並列に処理されます。findAny() は一番最初に終了した要素を取り出すと考えてください。この中で一番小さな値 29 が最初に計算を終了するので、findAny() で取り出す値は fibo(29) = 5142291 になります。並列処理でなければ findFirst() と同じです。findFirst() は並列処理に関係なく先頭要素の値を取り出します。並列処理でなければ、findAny() でも先頭の値 fibo(36) = 14930352 を取り出すことになります。

●合計値を求める

合計値を求める処理は並列処理で高速化することができます。たとえば、フィボナッチ数の合計値を求める処理は次のようになります。

リスト : 並列処理 (2)

import java.util.Arrays;
import java.util.List;

public class sample152 {
  static long fibo(long n) {
    if (n < 2)
      return n;
    else
      return fibo(n - 2) + fibo(n - 1);
  }
  
  public static void main(String[] args) {
    List<Long> xs = Arrays.asList(39L, 38L, 37L, 36L, 35L, 34L, 33L, 32L);
    long start = System.currentTimeMillis();
    System.out.println(fibo(39));
    long end = System.currentTimeMillis();
    System.out.println((end - start)  + "ms");
    start = System.currentTimeMillis();
    System.out.println(xs.stream().mapToLong(x -> fibo(x)).sum());
    end = System.currentTimeMillis();
    System.out.println((end - start)  + "ms");
    start = System.currentTimeMillis();
    System.out.println(xs.parallelStream().mapToLong(x -> fibo(x)).sum());
    end = System.currentTimeMillis();
    System.out.println((end - start)  + "ms");

  }
}
$ javac sample152.java
$ java sample152
63245986
358ms
162055563
931ms
162055563
506ms

実行環境 : Ubunts 18.04 (WSL), Intel Core i5-6200U 2.30GHz

一番時間がかかる処理は fibo(39) で約 0.36 秒かかります。実行時間はそれよりも速くなることはありませんが、並列に処理することで実行時間は約 1.8 倍高速になりました。

ところで、合計値は reduce() でも求めることができます。このとき、初期値を 0 にしないと正しい値を求めることができません。次のリストを見てください。

リスト : 合計値を求める (2)

import java.util.Arrays;
import java.util.List;

public class sample153 {
  static long fibo(long n) {
    if (n < 2)
      return n;
    else
      return fibo(n - 2) + fibo(n - 1);
  }
  
  public static void main(String[] args) {
    List<Long> xs = Arrays.asList(38L, 37L, 36L, 35L, 34L, 33L, 32L, 31L);
    System.out.println(xs.parallelStream().mapToLong(x -> fibo(x)).reduce(0L, (a, b) -> a + b));
    System.out.println("--- parallel ---");
    System.out.println(xs.parallelStream().mapToLong(x -> fibo(x)).reduce(10L, (a, b) -> {System.out.println(a + "," + b); return a + b;}));
    System.out.println("--- sequence ---");
    System.out.println(xs.stream().mapToLong(x -> fibo(x)).reduce(10L, (a, b) -> {System.out.println(a + "," + b); return a + b;}));

  }
}
$ javac sample153.java
$ java sample153
100155846
--- parallel ---
10,1346269
10,2178309
2178319,1346279
10,3524578
10,5702887
5702897,3524588
9227485,3524598
10,14930352
10,9227465
14930362,9227475
10,24157817
10,39088169
39088179,24157827
63246006,24157837
87403843,12752083
100155926
--- sequence ---
10,39088169
39088179,24157817
63245996,14930352
78176348,9227465
87403813,5702887
93106700,3524578
96631278,2178309
98809587,1346269
100155856

初期値を 10 に設定すると、合計値は 80 だけ増えてしまいます。これは並列に fibo() を計算したあと、必ず初期値 10 と fibo() の値を足し算するためです。シーケンスに処理するならば、 10 + fibo(38) を計算して、その値に fibo(37) を加算していくので、10 だけ増えることになります。並列処理で reduce() を使う場合はご注意くださいませ。

●参考 URL

  1. パッケージjava.util.stream, (Oracle, API ドキュメント)

初版 2016 年 12 月 4 日
改訂 2021 年 2 月 21 日

Copyright (C) 2016-2021 Makoto Hiroi
All rights reserved.

[ PrevPage | Java | NextPage ]