覚えたら書く

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

Lightweight-Stream-API - 色々なStream操作

Lightweight-Stream-APIを使うことでJava6やJava7でもStream APIを利用可能となります。

が、Lightweight-Stream-APIは単純にStream APIを代替するだけでなく、Java標準では提供されていないStream操作も提供してくれています。
本エントリでは、Java標準では提供されていないStream操作についてのサンプルを記載します


Stream#sample - N個おきの要素を取得する

Stream#sampleはStream内の要素をN(引数の値)個おきに取得するメソッドです

以下サンプルでは、0~20の値範囲のStreamから3個おき(0番目, 3番目, 6番目・・・)の値を取得しています

■サンプルコード

import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;


List<Integer> destList =
        Stream.rangeClosed(0, 20)
            .sample(3)
            .collect(Collectors.toList());


System.out.println(destList);

■実行結果

[0, 3, 6, 9, 12, 15, 18]


Stream#slidingWindow - スライドしながら組にする

Stream#slidingWindowは、要素を1つずつスライドしながら指定の要素数の集合を作ります。(スライドする数を指定することもできます)
これに関しては日本語での説明は難しいので、サンプルコードと実行結果を見てもらった方が分かりやすいです。。。

以下サンプルでは、0~10の値範囲から4要素の集合を要素をスライドしながら作っています。

■サンプルコード

import com.annimon.stream.Stream;


// 4個ずつの組を1要素ずつスライドしながら作る
Stream.rangeClosed(0, 10)
        .slidingWindow(4)
        .forEach(System.out::print);


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


// 4個ずつの組を2要素ずつスライドしながら作る
Stream.rangeClosed(0, 10)
        .slidingWindow(4, 2)
        .forEach(System.out::print);

■実行結果

[0, 1, 2, 3][1, 2, 3, 4][2, 3, 4, 5][3, 4, 5, 6][4, 5, 6, 7][5, 6, 7, 8][6, 7, 8, 9][7, 8, 9, 10]
---------------
[0, 1, 2, 3][2, 3, 4, 5][4, 5, 6, 7][6, 7, 8, 9][8, 9, 10]


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

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

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

■サンプルコード

import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;


List<String> destList1 =
                Stream.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 com.annimon.stream.Collectors;
import com.annimon.stream.Stream;


List<String> destList2 =
        Stream.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(com.annimon.stream.Optional)型で返されます(要素が存在しない場合もあるので)。

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

■サンプルコード

import com.annimon.stream.Stream;


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

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


// 先頭要素が見つからない場合
String name2 =
        Stream.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#chunkBy - 条件に一致したもの同士で分割する

Stream#chunkByを利用することで、対象の条件に合致するグループごとに分割することができます。
基本的に本操作をする前に、対象のStreamは条件に応じたソートが実施済みである必要があります。

サンプルでは以下2つを実施しています。

  • 名前(文字列)の集合をlengthが同じ物ごと分割
  • 数値を5の倍数とそれ以外ごとに分割

■サンプルコード

import com.annimon.stream.Stream;

// (1) lengthが同じものどうしてパーティショニングする
Stream.of("jon", "ken", "yui", "Taro", "Jiro", "Makoto", "Hanako", "Daigoro")
        .chunkBy(String::length)
        .forEach(System.out::print);


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


// (2) 5の倍数とそれ以外でパーティショニングする
Stream.of(1, 2, 5, 6, 7, 9, 10, 12, 14, 15, 20, 25, 31)
        .chunkBy(i -> i % 5 == 0)
        .forEach(System.out::print);

■実行結果

[jon, ken, yui][Taro, Jiro][Makoto, Hanako][Daigoro]

--------------
[1, 2][5][6, 7, 9][10][12, 14][15, 20, 25][31]


Stream#indexed - 要素をindexとペアにする

Stream#indexedを利用するとStream内の各要素をindex番号と組にしたIntPairで取り扱うことができます。
IntPairクラスは、Lightweight-Stream-APIによって提供されます。

■サンプルコード

import com.annimon.stream.Stream;


Stream.of("jon", "ken", "yui", "Taro", "Jiro", "Makoto", "Daigoro")
        .indexed()
        .forEach(System.out::println);

■実行結果

IntPair[0, jon]
IntPair[1, ken]
IntPair[2, yui]
IntPair[3, Taro]
IntPair[4, Jiro]
IntPair[5, Makoto]
IntPair[6, Daigoro]


Stream#zip - 要素同士を合わせる

Stream#zipは2つのStream内の同じ位置の要素同士を合わせる操作を行うものです。

以下サンプルでは、IntegerのStreamで各要素同士で足し算を行う操作をしています。

■サンプルコード

import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;


List<Integer> list1 = Arrays.asList(0, 1, 2, 3, 4, 5);
List<Integer> list2 = Arrays.asList(12, 13, 14, 15, 16, 17);

List<Integer> zippedList1 =
        Stream.zip(Stream.of(list1), Stream.of(list2), (a, b) -> a + b)
            .collect(Collectors.toList());

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


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

// 元がListならIterator使うことでも要素の組み合わせられる
List<Integer> zippedList2 =
        Stream.zip(list1.iterator(), list2.iterator(), (a, b) -> a + b)
            .collect(Collectors.toList());

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

■実行結果

zippedList1: [12, 14, 16, 18, 20, 22]
-------------------------
zippedList2: [12, 14, 16, 18, 20, 22]


Stream#custom - Streamに独自実装のoperatorを適用する

Stream#customは、Streamに自分で実装したoperatorを適用することができます

以下サンプルでは、独自の3つのOperatorを定義してそれをStreamに適用しています。

■サンプルコード

import com.annimon.stream.Stream;
import com.annimon.stream.function.BinaryOperator;
import com.annimon.stream.function.Function;
import com.annimon.stream.function.UnaryOperator;


// 逆順にするOperator
static final class Reverse<T> implements UnaryOperator<Stream<T>> {

    @Override
    public Stream<T> apply(Stream<T> stream) {
        final Iterator<? extends T> iterator = stream.iterator();
        final ArrayDeque<T> deque = new ArrayDeque<T>();
        while (iterator.hasNext()) {
            deque.addFirst(iterator.next());
        }
        return Stream.of(deque.iterator());
    }
}


// skipとlimitをまとめて実行するOperator
static final class SkipAndLimit<T> implements UnaryOperator<Stream<T>> {

    private final int skip;
    private final int limit;

    public SkipAndLimit(int skip, int limit) {
        this.skip = skip;
        this.limit = limit;
    }

    @Override
    public Stream<T> apply(Stream<T> stream) {
        return stream.skip(skip).limit(limit);
    }
}

// intの合計を計算するためのoperator(終端操作)
static final class Sum implements Function<Stream<Integer>, Integer> {

    @Override
    public Integer apply(Stream<Integer> stream) {
        return stream.reduce(0, new BinaryOperator<Integer>() {
            @Override
            public Integer apply(Integer value1, Integer value2) {
                return value1 + value2;
            }
        });
    }
}



// Reverse を適用(Streamを逆順にする)
Stream.of("jon", "ken", "yui", "Taro", "Jiro", "Makoto", "Daigoro")
    .custom(new Reverse<>())
    .forEach(System.out::println);

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

// SkipAndLimit を適用(先頭2つ飛ばして、最大3この要素を取得)
Stream.of("jon", "ken", "yui", "Taro", "Jiro", "Makoto", "Daigoro")
    .custom(new SkipAndLimit<>(2, 3))
    .forEach(System.out::println);

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

// Sum を適用(intの合計値を計算)
int sum = Stream.of(1, 3, 5, 7, 9)
    .custom(new Sum())
    .intValue();

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

■実行結果

Daigoro
Makoto
Jiro
Taro
yui
ken
jon
-------------
yui
Taro
Jiro
-------------
sum: 25


まとめ

Lightweight-Stream-APIには、Java8標準のStream APIにはない操作が提供されていることが分かりました。
これらを使うことでStream操作がさらに簡潔になったり明示的になります。
さらに、これら操作はJava6やJava7環境でも動作するので、とてもありがたいです。



関連エントリ