Java
java8
Stream
StreamAPI
Java9

Java8/9 初心者向け:Stream APIのハマりポイントと対応方法

Java8から実装されたStream APIは非常に便利な機能ではあるものの、
Stream特有の処理にハマってしまうことが多いので、対応策を記載する

1.そもそもStreamの呼び出しがわからない

基本的に、下記のパターンだけ覚えていれば十分
・CollectionからStreamに変換する
・配列&可変長引数からStreamに変換する
・IntStreamの呼び出し

※Collection→Streamはインスタンスメソッド、配列→Streamはstaticメソッドを使う

基本的なStream呼び出し
// Collection(List/Set等)の場合
Collection<BigDecimal>list = List.of(BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.TEN);
Stream<BigDecimal>stream = list.stream();

// 配列の場合
BigDecimal[] array = new BigDecimal[] {BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.TEN};
Stream<BigDecimal>stream = Stream.of(array);
// 可変長引数の場合(配列の場合と同じメソッドでOK)
Stream<BigDecimal>stream = Stream.of(BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.TEN);

// intのループ処理を行う場合 ※ for(int i=0; i<=100 i++)...と同等の処理
IntStream stream = IntStream.rangeClosed(0, 100);

2.知らなくて発生しがちなハマりポイント

Stream始めたて時に発生しがちな事象
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed

2.1 :IllegalStateExceptionが発生...Streamが再利用できない

Streamは、処理後に必ず別の型に変換(終端処理)しなければならない
ListのようにStreamの状態で置いておく・再利用することは不可能

Streamは再利用できない
Stream<String>stream = Stream.of("","","");
// 何らかの処理を行うと、Streamが閉じてしまう(回避方法はない)
stream.collect(Collectors.toCollection(ArrayDeque::new));
// すでに閉じているStreamは再利用できない
stream.count();

// スタックトレース
//Exception in thread "main" java.lang.IllegalStateException: stream has already been //operated upon or closed
//  at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
//  at java.base/java.util.stream.ReferencePipeline.count(ReferencePipeline.java:538)
//  at main.TestFunc.main(TestFunc.java:11) 

対応1:変数や戻り値にStreamを使わない。Collection(List等)かMapを使い、Streamに毎回変換する

回避例
// 何かのコレクションで変数を置いてから...
List<BigDecimal> list = List.of(BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.TEN);
// リストに変換する
Deque<BigDecimal> deque = list.stream().collect(Collectors.toCollection(ArrayDeque::new));
// 件数を数える。各処理ごとにStreamを作成する
long count = list.stream().count();

対応2:(特殊例)Average・Sum・Countなどの処理はまとめて処理できる。

SummaryStatisticsの活用
List<BigDecimal> list = List.of(BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.TEN);

// SummaryStatisticsに変換する。なお、DecimalSummaryStatisticsは存在しない
IntSummaryStatistics summary = list.stream().collect(Collectors.summarizingInt(BigDecimal::intValueExact));

// 一度のStream処理から、複数の結果を得られる
summary.getMax();
summary.getAverage();
summary.getSum();

2.2 Streamに型を限定したメソッドがない

例えば、Stream<Integer>に対してsum()するメソッドがない。

対応メソッドがない例
// このようなメソッドがない
Stream.of(1,2,3).sum();

対応1(基本型の場合):基本型専用の特殊Streamを使用する

IntStreamに変換する例
// mapToIntでIntStreamに変換する→IntStreamが特別に持つメソッドを使用する
Stream.of(1, 2, 3).mapToInt(e -> e).sum();

対応2(基本型以外の場合):StreamのメソッドかCollectorを利用して処理を行う

Decimalで加算する例
// パターン1:初期値0で、Stream内のDecimal値を全て足す処理=合計を求める処理を行う。従来のfor文に近い処理方法
Stream.of(1, 2, 3).map(BigDecimal::valueOf).reduce(BigDecimal.ZERO,BigDecimal::add);

// パターン2:Collectorを準備しておき、それを使用する。何度も再利用したい場合に有効
Collector<BigDecimal, ?, BigDecimal> sum = Collectors.reducing(BigDecimal.ZERO, BigDecimal::add);

Stream.of(1, 2, 3).map(BigDecimal::valueOf).collect(sum);
Stream.of(BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.TEN).collect(sum);

2.3 配列・List・Mapへの戻し方がわからない

基本的に、下記のパターンだけ覚えていれば十分
・StreamからCollectionに変換する
・Streamから配列に変換する

Streamからの変換
// List(指定なし)の場合
List<BigDecimal> list = Stream.of(BigDecimal.ZERO, BigDecimal.ONE).limit(1).collect(Collectors.toList());
// その他コレクションの場合
Deque<BigDecimal> deque = Stream.of(BigDecimal.ZERO, BigDecimal.ONE).limit(1).collect(Collectors.toCollection(LinkedList::new));

// 配列の場合
BigDecimal[] array = Stream.of(BigDecimal.ZERO, BigDecimal.ONE).limit(1).toArray(BigDecimal[]::new);

2.4 Optionalの処理方法がわからない

FindFirstメソッドでは、結果がnullかもしれないため、Optionalで囲ってプログラマーにnull時の処理を定義するよう促している。
なお、Optionalの状態では中身(下例ではBigDecimal)に対して
今のところ、Stream以外ではこのような処理方式は行われていないのでハマりやすい

Optionalを処理しなければならない例
// エラー・・・[型の不一致: Optional<BigDecimal> から BigDecimal には変換できません]
BigDecimal first = Stream.of(BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.TEN).findFirst();

対応:Optionalから中身を取り出すメソッドを付ける

Optionalを処理する例
// 要素がないことも想定している場合(今回は、要素がなければ0とする)
BigDecimal first = Stream.of(BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.TEN).findFirst().orElse(BigDecimal.ZERO);

// 異常が発生しない限り要素なしにならない場合(NoSuchElementExceptionをスローする)
BigDecimal first = Stream.<BigDecimal>of().findFirst().get();
// 異常が発生しない限り要素なしにならない場合(指定した例外をスローする)
BigDecimal first = Stream.<BigDecimal>of().findFirst().orElseThrow(IllegalStateException::new);