KUSAMAKURA

智に働けば角が立つ。情に棹させば流される。意地を通せば窮屈だ。とかくに人の世は住みにくい。

Java8 で導入された Stream の使い方をはじめて使ってみる人向けに体系的に説明してみた

Stream とは

Java8 より、新規に追加された stream パッケージのこと。Collection に対する処理(集計、変換)を宣言的に記述することをサポートする。 パッケージは、java.util.stream となっており、Collection のインターフェースとは別パッケージとなっている。 日本語APIはこちら

オブジェクトに対する Stream と、プリミティブに対する Stream

対象とする Collection の種別により、異なる Stream インターフェースが用意されている。

オブジェクトに対する Stream

interface Stream

@Test
public void objectStream_sum() {
  List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
  int sum = list.stream().mapToInt(Integer::intValue).sum();
  assertThat(sum).isEqualTo(55);
}

プリミティブに対する Stream

interface IntStream, interface DoubleStream, interface LongStream

@Test
public void intStream_sum() {
  // 1 - 100 までの総和
  int sum = IntStream.rangeClosed(1, 100).sum();
  assertThat(sum).isEqualTo(5050);
}
@Test
public void doubleStream_sum() {
  // double 値の総和
  double sum = DoubleStream.of(2.5d, 3.7d, 4.98d).sum();
  assertThat(sum).isEqualTo(11.18);
}

Stream メソッドの種類

Stream のメソッドは、大きく中間処理と、末端処理の2種類に分けられる。中間処理は、処理の終わりに Stream 自体を返却するメソッド。それ以外のメソッドを終端処理と呼ぶ。「オブジェクトに対する Stream」の例にある、mapToInt() が中間処理、sum() が終端処理となる。

中間処理

中間処理は、filter()map()など、処理の終わりに Stream 自体を返却するメソッド。Stream 内に保持されている値に対しての操作は、中間処理中は実施されない。

終端処理

終端処理は、count()anyMatch() など、処理の終わりに、処理結果を返却する。 終端処理の呼び出しの際に、実際の処理が実施される。そのため、大量データの処理であっても実行時間がかかるのは、終端処理となり、中間処理はそれ程時間はかからない。

Stream インターフェースのメソッド分類

Stream インターフェースのメソッドを static、中間処理、終端処理、それ以外に分類した表が次になる。例えば、終端処理を眺めるだけでも、最終的にどのようなことができるのか見通しが良くなると思う。

static 中間処理 終端処理 その他
builder allMatch collect forEach
concat anyMatch count forEachOrdered
empty distinct findAny
generate filter findFirst
iterate flatMap max
of flatMapToDouble min
flatMapToInt noneMatch
flatMapToLong reduce
limit toArray
map
mapToDouble
mapToInt
mapToLong
peek
skip
sorted

Stream の使い方

ここまで、Stream とは何かを体系から説明してきた。Stream とは、Collection に対して変換、集計を実施する。また、オブジェクトとプリミティブでは、異なる Stream を利用する。Stream メソッドには、中間処理、終端処理があるんだと理解できていれば良いと思う。 ここでは、それらを踏まえて、実際に Stream の使い方を理解する。

Stream の使い方は至ってシンプルだ。下記の3Stepのみである。上から下に流れて行き、逆流はできない。

  1. Stream を作る
  2. 中間処理を行う
  3. 終端処理を行う
@Test
public void stream_step() {
  List<String> nuumbers = Arrays.asList("1","2","3","4","5","6","7","8","9","10");
  int sum = nuumbers.stream()         // 1. numbers collection から、 Stream<String> を作る
      .mapToInt(Integer::parseInt)    // 2. 中間処理で、Stringから、int に変換
      .sum();                         // 3. 終端処理で、総和を求める
  
  assertThat(sum).isEqualTo(55);
}

中間処理は、Stream 自身を返却するため、何度も適用できる。先の例に、fileter の中間処理を追加してみる。

@Test
public void stream_intemediate_operation_any() {
  List<String> nuumbers = Arrays.asList("1","2","3","4","5","6","7","8","9","10");
  int sum = nuumbers.stream()         // 1. numbers collection から、 Stream<String> を作る
      .mapToInt(Integer::parseInt)    // 2-1. 中間処理で、Stringから、int に変換
      .filter(v -> v % 2 == 0)        // 2-2. 中間処理で、偶数のみ抽出
      .sum();                         // 3. 終端処理で、総和を求める
  
  assertThat(sum).isEqualTo(30);
  
}

よく使う中間処理と終端処理

よく使う、中間処理と終端処理の使い方を記載する。

Collection の中身を利用して、何かの値を作成し、リストに変換

先ずは、Collection の中身を利用して、何かの値を作成し、リストに変更するパターン。殆どのループがこれなのではないだろうか。
例では、中間処理の map で、 key と value を結合し、表示用の文字列を返却している。終端処理の collect で、List にまとめて出来上がり。

  @Value
  private static class Hoge {
    private String key;
    private String value;
  }
  
  @Test
  public void stream_map() {
    List<Hoge> hoges = new ArrayList<>();
    Hoge hoge1 = new Hoge("hoge1", "HOGE1");
    hoges.add(hoge1);
    Hoge hoge2 = new Hoge("hoge2", "HOGE2");
    hoges.add(hoge2);
    Hoge hoge3 = new Hoge("hoge3", "HOGE3");
    hoges.add(hoge3);
    
    List<String> dispNames = hoges.stream()                                                  
        .map(v -> new Formatter().format("%s: %s", v.getKey(), v.getValue()).toString())     
        .collect(Collectors.toList());
    
    assertThat(dispNames.get(0)).isEqualTo("hoge1: HOGE1");
            
    
  }
Collection の中身をフィルタリングし、オブジェクトを生成し、リストに変換

上の「Collection の中身を利用して、何かの値を作成し、リストに変換」に似ているが、オブジェクトの生成に注目して欲しい(map(new)のところ)。むしろ、fileter はおまけ。
Stream とは直接関係無いが、オブジェクトの生成を外に出すことにより、非常に見通しの良いコードになっている。別に For ループでもそう書くって? しかし、経験上、ループ中の new からの Setter 乱舞は非常に良く見かける。
ラムダ式の恩恵なのだが、Stream 中にそのような暴挙をみることがないのは、嬉しい副産物だと思う。

@Value
private static class Hoge {
  private String key;
  private String value;
}

@Test
public void stream_newInstance() {
  Map<String, String> dictionary = new HashMap<>();
  dictionary.put("hoge1", "HOGE1");
  dictionary.put("hoge2", "HOGE2");
  dictionary.put("hoge3", "HOGE3");
  
  List<Hoge> hoges = dictionary.entrySet().stream()
      .filter(v->v.getKey().equals("hoge2"))
      .map(v -> new Hoge(v.getKey(), v.getValue()))     
      .collect(Collectors.toList());
  
  assertThat(hoges.get(0).getValue()).isEqualTo("HOGE2");
          
  
}

まとめ

Stream とは、Collection に対する処理を宣言的に記述できるもの。
中間処理と終端処理があり、逆流はできない。
ある値の Collection をある値の Collection に変換する Dxo のような処理には、非常にパワフルに働く。もちろん、それ以外でもだけど。
Stream は、ラムダ式(v -> ってやつ)や、メソッド参照(:: ってやつ)のお陰で取っ付き難い印象があるが、殆どの Collection 処理は、もうコレで良いのでは無いかと思うほど、力強い。 短く簡潔な宣言的なコードには、バグが入り込む余地は少なくなり、コードを読む時間も短縮できる。良いこと尽くめなので、毛嫌いせずに、是非、導入して頂きたい。