覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

streamsupport - Stream APIのバックポートライブラリ

Java8のStream APIと同等の機能を提供するライブラリとして、Lightweight-Stream-API紹介エントリを書きましたが、
これ以外にもStream APIと同等の機能を提供するライブラリとしてstreamsupportというものも存在しています。

Lightweight-Stream-APIと立ち位置的には似ていて、基本的にJava6やJava7環境へStream APIをバックポートするためのライブラリとなっています。
(ちなみに、名前がかぶっていてややこしいですが、Java標準のStreamSupportクラスのことではありません。)


streamsupportの利用準備

Mavenプロジェクトでstreamsupportから提供されるStream APIの代替機能を利用する場合、pom.xmlに以下を追記します。

<dependency>
    <groupId>net.sourceforge.streamsupport</groupId>
    <artifactId>streamsupport</artifactId>
    <version>1.5.2</version>
</dependency>

また、最終的に生成したモジュールを実行する環境がJava6やJava7である場合は、Retrolambdaの設定も合わせて実施してください。(これによりラムダ式等を組み合わせることができます)


以下の各種サンプルでは基本的にRetrolambdaを組み合わせることを前提にした(ラムダ式やメソッド参照を利用した)コードを記述しています


Java8のStream APIの代わりに使う場合の基本的な使用方法

Java8のStream APIで提供されている操作は、streamsupportでもほとんど同名の操作として提供されています。
そのためJava8のStream APIでのStream操作は、streamsupportで置き換えることができます。

例えばStream操作を行う場合のJava8標準とstreamsupportの対応関係は以下表のようになります

やりたいこと Java8標準 streamsupport
CollectionからStreamへの変換 java.util.Collection#stream java8.util.stream.StreamSupport#stream
配列からStreamへの変換 java.util.Arrays#stream java8.util.J8Arrays#stream
Streamのfilter / map / reduce / sort操作 java.util.stream.Stream#filter, map, reduce, sorted java8.util.stream.Stream#filter, map, reduce, sorted
StreamからListへの変換 java.util.stream.Stream#collect java8.util.stream.Stream#collect

基本的に同名の操作で提供されると書きましたが、例えばCollectionからStreamへの変換操作等は、異なる名前の操作で提供されています


以下にJava8標準とstreamsupportを用いた場合の具体的なサンプルコードを比較して載せています。
(基本的にjava8.util.streamパッケージを利用することで、Java標準のStream APIとほぼ同じコードで実行できることが分かります)


■ListからStreamを生成して操作する

名前(文字列)のListに対して以下の操作をするサンプルです

  • 特定の文字列(<unknown>)を除外 (filter)
  • 文字列を大文字に変換 (map)
  • 辞書順でソート (sorted)
  • 各々の要素(文字列)を標準出力に出力する (forEach)

Java8標準の場合

import java.util.Arrays;


java.util.List<String> nameList = Arrays.asList("ken", "<unknown>", "taro", "ayumi", "hanako", "jin", "makoto", "<unknown>");

nameList.stream()
    .filter(s -> !s.equals("<unknown>"))
    .map(s -> s.toUpperCase())
    .sorted()
    .forEach(System.out::println);

streamsupportの場合

import java.util.Arrays;
import java8.util.stream.StreamSupport;


List<String> nameList = java.util.Arrays.asList("ken", "<unknown>", "taro", "ayumi", "hanako", "jin", "makoto", "<unknown>");

StreamSupport.stream(nameList)
    .filter(s -> !s.equals("<unknown>"))
    .map(s -> s.toUpperCase())
    .sorted()
    .forEach(System.out::println);

実行結果(Java8標準, streamsupport のいずれの場合も同じ)

AYUMI
HANAKO
JIN
KEN
MAKOTO
TARO


■配列からStreamを生成して操作する

名前(文字列)の配列に対して以下の操作をするサンプルです

  • 特定の文字列(<unknown>)を除外 (filter)
  • 文字列の文字数に変換(取得) (map)
  • 値の大きい順(文字数の多い順)にソート (sorted)
  • 重複(文字数が同じ)要素を除外(distinct)
  • 結果をListに格納 (collect)

Java8標準の場合

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


String[] srcArray = new String[] {"ken", "<unknown>", "taro", "ayumi", "hanako", "jin", "makoto", "<unknown>"};

java.util.List<Integer> destList =
    Arrays.stream(srcArray)
        .filter(s -> !s.equals("<unknown>"))
        .map(s -> s.length())
        .sorted((x, y) -> y - x)
        .distinct()
        .collect(Collectors.toList());

System.out.println("destList: " + destList);

streamsupportの場合

import java8.util.J8Arrays;
import java8.util.stream.Collectors;


String[] srcArray = new String[] {"ken", "<unknown>", "taro", "ayumi", "hanako", "jin", "makoto", "<unknown>"};

java.util.List<Integer> destList =
    J8Arrays.stream(srcArray)
        .filter(s -> !s.equals("<unknown>"))
        .map(s -> s.length())
        .sorted((x, y) -> y - x)
        .distinct()
        .collect(Collectors.toList());

System.out.println("destList: " + destList);

実行結果(Java8標準, streamsupport のいずれの場合も同じ)

destList: [6, 5, 4, 3]


■IntStreamを操作する

1~10の値範囲のIntStreamを生成してその中の偶数の合計値を計算すサンプルです

Java8標準の場合

import java.util.stream.IntStream;


int sum =
        IntStream.rangeClosed(1, 10)
            .filter(i -> i % 2 == 0)
            .sum();

System.out.println("sum: " + sum);

streamsupportの場合

import java8.util.stream.IntStreams;


int sum =
        IntStreams.rangeClosed(1, 10)
            .filter(i -> i % 2 == 0)
            .sum();

System.out.println("sum: " + sum);

実行結果(Java8標準, streamsupport のいずれの場合も同じ)

sum: 30


■Streamを並列処理する

streamsupportは、Java標準のStream APIと同様に並列処理をサポートしています。
(ちなみに、Lightweight-Stream-APIの場合はこの機能を提供していません)

Java8標準の場合

import java.util.Arrays;


java.util.List<String> nameList = Arrays.asList("ken", "<unknown>", "taro", "ayumi", "hanako", "jin", "makoto", "<unknown>");

// Stream#parallelStreamを使う場合
nameList.parallelStream()
    .filter(s -> !s.equals("<unknown>"))
    .map(s -> s.toUpperCase())
    .forEach(System.out::println);

System.out.println("----");

// Stream#parallelを使う場合
nameList.stream()
    .parallel()
    .filter(s -> !s.equals("<unknown>"))
    .map(s -> s.toLowerCase())
    .forEach(System.out::println);

streamsupportの場合

import java.util.Arrays;
import java8.util.stream.StreamSupport;


java.util.List<String> nameList = Arrays.asList("ken", "<unknown>", "taro", "ayumi", "hanako", "jin", "makoto", "<unknown>");

// StreamSupport#parallelStreamを使う場合
StreamSupport.parallelStream(nameList)
    .filter(s -> !s.equals("<unknown>"))
    .map(s -> s.toUpperCase())
    .forEach(System.out::println);

System.out.println("----");

// Stream#parallelを使う場合
StreamSupport.stream(nameList)
    .parallel()
    .filter(s -> !s.equals("<unknown>"))
    .map(s -> s.toLowerCase())
    .forEach(System.out::println);

実行結果(並列処理しているので実行のたびに結果が変わります)

JIN
HANAKO
MAKOTO
KEN
AYUMI
TARO
----
taro
ken
jin
hanako
makoto
ayumi


Stream APIに存在しないStream操作

streamsupportは、Java8標準のStream APIには存在しないStream操作も提供します


Stream#takeWhile - 条件に一致している間は取得する

Stream#takeWhileは、Stream内の先頭要素から順に対象の条件に一致している間は値を取得しますが、
条件に一致しない要素が現れたタイミングで取得をしなくなります。

以下サンプルは、lengthが2以下の文字列が表れている間は取得し、それより大きい値が現れた瞬間に取得をしなくなる例です

サンプルコード

import java8.util.stream.Collectors;
import java8.util.stream.RefStreams;


List<String> destList1 =
                RefStreams.of("a", "b", "c", "de", "fgh", "ijkl", "mno", "pq", "r", "s")
                    .takeWhile(s -> s.length() <= 2)
                    .collect(Collectors.toList());

System.out.println(destList1);

実行結果

[a, b, c, de]

5番目の要素が条件に一致しなかったのでそれ以降の値は全て無視した状態になっています


Stream#dropWhile - 条件に一致している間は除外する

Stream#dropWhileは、Stream内の先頭要素から順に対象の条件に一致している間は値を除外し、
条件に一致しない要素が現れたタイミングから取得を開始します。
Stream#takeWhileのが逆のような操作になります。)

以下サンプルは、lengthが2以下の文字列が表れている間は除外して、それより大きい値が現れた瞬間に取得を開始する例です

サンプルコード

import java8.util.stream.Collectors;
import java8.util.stream.RefStreams;


List<String> destList2 =
        RefStreams.of("a", "b", "c", "de", "fgh", "ijkl", "mno", "pq", "r", "s")
            .dropWhile(s -> s.length() <= 2)
            .collect(Collectors.toList());


System.out.println(destList2);

実行結果

[fgh, ijkl, mno, pq, r, s]

4番目の要素が条件に一致しなかったのでそれ以降の値を取得した状態になっています


Stream#findFirst - Stream内の先頭要素を取得する

Stream#findFirstを使うことでStream内の先頭要素を取得することができます。
findFirstの結果は、Optional(java8.util.Optional)型で返されます(要素が存在しない場合もあるので)。

以下サンプルでは、文字列のStreamからlengthが一定以上のものにfilterしてその中からfindFirstで先頭の値を取得しています。
要素が存在しない場合はOptional#orElseメソッドで、"##None##"という文字列を返すようにしています

サンプルコード

import java8.util.stream.RefStreams;


// 先頭要素が見つかる場合
String name1 =
        RefStreams.of("jon", "ken", "yui", "Taro", "Jiro", "Makoto", "Daigoro")
            .filter(s -> s.length() > 3)
            .findFirst()
            .orElse("##None##");

System.out.println("filtered result1: " + name1);


// 先頭要素が見つからない場合
String name2 =
        RefStreams.of("jon", "ken", "yui", "Taro", "Jiro", "Makoto", "Daigoro")
            .filter(s -> s.length() > 10)
            .findFirst()
            .orElse("##None##");

System.out.println("filtered result2: " + name2);

実行結果

filtered result1: Taro
filtered result2: ##None##


Stream操作以外にも

streamsupportはJava標準のStream APIそのものをバックポートしているだけではなく以下のようなAPI(クラス、メソッド)についてもバックポートしています。

Java標準 streamsupport
java.util.Optional java8.util.Optional
java.util.StringJoiner java8.util.StringJoiner
java.util.Objects java8.util.Objects


streamsupportが提供するさらなる機能

streamsupportはさらに以下のような機能についてもバックポートしています。

  • streamsupport-cfuture
    • CompletableFuture API
  • streamsupport-atomic
    • java.util.concurrent.atomicパッケージ
  • streamsupport-flow
    • JDK 9 Flow API
  • streamsupport-literal

これらについては以下エントリを参照ください。


まとめ

streamsupportを利用することでJava6, Java7を実行環境とする場合でも、Java8で提供されるStream API等の機能をほぼ取り込んだ状態の開発ができることが分かりました。



関連エントリ