覚えたら書く

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

Lightweight-Stream-API - Java6・7でもStream APIを使いたい

Java8から提供されているStream APIをJava6やJava7でも利用可能にしてくれるライブラリとしてLightweight-Stream-APIが存在しています。

このライブラリを使うことでJava8と同等かそれ以上の操作を利用することが可能となります。


Lightweight-Stream-APIの利用準備

MavenプロジェクトでLightweight-Stream-APIを利用する場合、pom.xmlに以下を追記します。

<dependency>
  <groupId>com.annimon</groupId>
  <artifactId>stream</artifactId>
  <version>1.1.3</version>
</dependency>

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


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


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

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

例えばCollectionをもとにしたStreamの操作を行う場合のJava8標準とLightweight-Stream-APIの対応関係は以下表のようになります

やりたいこと Java8標準 Lightweight-Stream-API
CollectionからStreamへの変換 java.util.Collection#stream com.annimon.stream.Stream#of
Streamのfilter / map / reduce / sort操作 java.util.stream.Stream#filter, map, reduce, sorted com.annimon.stream.Stream#filter, map, reduce, sorted
StreamからListへの変換 java.util.stream.Stream#collect com.annimon.stream.Stream#collect

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


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


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

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

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

Java8標準の場合

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


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);

Lightweight-Stream-APIの場合

import java.util.Arrays;
import java.util.List;
import com.annimon.stream.Stream;


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

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

実行結果(Java8標準, Lightweight-Stream-API のいずれの場合も同じ)

AYUMI
HANAKO
JIN
KEN
MAKOTO
TARO


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

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

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

Java8標準の場合

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


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

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

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

Lightweight-Stream-APIの場合

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

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


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

List<Integer> destList =
    Stream.of(nameList)
        .filter(s -> !s.equals("<unknown>"))
        .map(s -> s.length())
        .sorted((x, y) -> y - x)
        .distinct()
        .collect(Collectors.toList());

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

実行結果(Java8標準, Lightweight-Stream-API のいずれの場合も同じ)

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);

Lightweight-Stream-APIの場合

import com.annimon.stream.IntStream;


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

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

実行結果(Java8標準, Lightweight-Stream-API のいずれの場合も同じ)

sum: 30


Streamの操作を明示的に簡潔に記述するためのメソッド群

Lightweight-Stream-APIは、Java標準のStream APIでは提供されていないStream操作を提供しています。
Lightweight-Stream-APIの操作を利用することでJava標準の場合よりも明示的に簡潔にコードを書くことが可能となります。

サンプル内にPersonクラスが登場しますが、以下のクラスとして定義されているものとします(省略のためLombokを利用)

import lombok.Value;

@Value
public class Person {

    private int id;

    private String name;
}


■Stream#filterNot - 否定形のfilter操作

Java標準のStream#filterを用いて特定の値を除外する否定形の操作をする場合は、若干分かりにくくなります。またメソッド参照を単純には使用できません。
Lightweight-Stream-APIでは、filterNotという否定形用のfilter操作が提供されています。

以下は、名前(文字列)のListから空文字列の値を取り除くサンプルです

Java8標準の場合

import java.util.function.Predicate;
import java.util.stream.Collectors;

List<String> nameList = Arrays.asList("Ken", "", "Taro", "Ayumi", "Hanako", "Iin", "Makoto", "");


// ラムダ式を使う場合
List<String> destList =
        nameList.stream()
            .filter(s -> !s.isEmpty())
            .collect(Collectors.toList());


// メソッド参照を無理やり使う場合
List<String> destList =
        nameList.stream()
            .filter(((Predicate<String>) String::isEmpty).negate())
            .collect(Collectors.toList());


System.out.println(destList);

Lightweight-Stream-APIの場合

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


List<String> nameList = Arrays.asList("Ken", "", "Taro", "Ayumi", "Hanako", "Iin", "Makoto", "");

List<String> destList =
        Stream.of(nameList)
            .filterNot(String::isEmpty)
            .collect(Collectors.toList());


System.out.println(destList);

実行結果(Java8標準, Lightweight-Stream-API のいずれの場合も同じ)

[Ken, Taro, Ayumi, Hanako, Iin, Makoto]


■Stream#select - 指定の型にマッチする値だけを抽出する

Lightweight-Stream-APIselectを使用すると複数のclassの値が混在するStreamから指定のクラスの要素だけを抽出することができます。

以下は、複数の型の値が格納されたListからInteger型の要素を抽出するサンプルです

Java8標準の場合

List<?> srcList = Arrays.asList("Str001", 123, new Date(), "Str002", 456, 789L);

List<?> destList =          //  <- List<integer> の型は記述できない
        srcList.stream()
            .filter(Integer.class::isInstance)
            .collect(Collectors.toList());


System.out.println(destList);

Lightweight-Stream-APIの場合

List<?> srcList = Arrays.asList("Str001", 123, new Date(), "Str002", 456, 789L);

List<Integer> destList =
        Stream.of(srcList)
            .select(Integer.class)
            .collect(Collectors.toList());


System.out.println(destList);

実行結果(Java8標準, Lightweight-Stream-API のいずれの場合も同じ)

[123, 456]


■Stream#sortBy - メソッドを指定してのソート

Lightweight-Stream-APIが提供するsortByを利用することで、特定のメソッドの結果でのソートを直観的に記述することが可能となります。

以下サンプルはPersonクラスのname順(getNameの結果による)ソートを行う例です

Java8標準の場合

import java.util.Comparator;
import java.util.stream.Collectors;


Person p1 = new Person(1, "Taro");
Person p2 = new Person(2, "Jiro");
Person p3 = new Person(5, "Hanako");
Person p4 = new Person(100, "Ayako");
Person p5 = new Person(11, "Beck");
Person p6 = new Person(3, "Taro");

List<Person> srcList = Arrays.asList(p1, p2, p3, p4, p5, p6);

List<Person> destList =
        srcList.stream()
            .sorted(Comparator.comparing(Person::getName))
            .collect(Collectors.toList());

System.out.println(destList);

Lightweight-Stream-APIの場合

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

Person p1 = new Person(1, "Taro");
Person p2 = new Person(2, "Jiro");
Person p3 = new Person(5, "Hanako");
Person p4 = new Person(100, "Ayako");
Person p5 = new Person(11, "Beck");
Person p6 = new Person(3, "Taro");

List<Person> srcList = Arrays.asList(p1, p2, p3, p4, p5, p6);

List<Person> destList =
        Stream.of(srcList)
            .sortBy(Person::getName)
            .collect(Collectors.toList());

System.out.println(destList);

実行結果(Java8標準, Lightweight-Stream-API のいずれの場合も同じ)

name順にソートされていることが分かります

[Person(id=100, name=Ayako), Person(id=11, name=Beck), Person(id=5, name=Hanako), Person(id=2, name=Jiro), Person(id=1, name=Taro), Person(id=3, name=Taro)]


■Stream#groupBy - メソッドを指定してgroup by

Lightweight-Stream-APIが提供するgroup byを利用することで、その名の通りにグループ化(group by)を直観的に実施することが可能となります。
Java標準のStream APIでは、中間操作としてのgroup byが無いため、終端操作のcollectでCollectors#groupingByを実施する必要があります。そのため、その後にさらにStream操作をしたい場合等はコードが単純ではなくなります。
これに対してLightweight-Stream-APIのgroup byは中間操作として提供されているため、このような問題がクリアされています。

以下サンプルはPersonクラスのname(getNameの結果による)でgroup byを実施する例です。

Java8標準の場合

import java.util.stream.Collectors;


Person p1 = new Person(1, "Taro");
Person p2 = new Person(2, "Jiro");
Person p3 = new Person(5, "Hanako");
Person p4 = new Person(100, "Ayako");
Person p5 = new Person(11, "Beck");
Person p6 = new Person(3, "Taro");
Person p7 = new Person(101, "Ayako");

List<Person> srcList = Arrays.asList(p1, p2, p3, p4, p5, p6, p7);

srcList.stream()
      .collect(Collectors.groupingBy(Person::getName))
      .entrySet()
      .stream()
      .forEach(System.out::println);

Lightweight-Stream-APIの場合

import com.annimon.stream.Stream;

Person p1 = new Person(1, "Taro");
Person p2 = new Person(2, "Jiro");
Person p3 = new Person(5, "Hanako");
Person p4 = new Person(100, "Ayako");
Person p5 = new Person(11, "Beck");
Person p6 = new Person(3, "Taro");
Person p7 = new Person(101, "Ayako");

List<Person> srcList = Arrays.asList(p1, p2, p3, p4, p5, p6, p7);

Stream.of(srcList)
    .groupBy(Person::getName)
    .forEach(System.out::println);

実行結果(Java8標準, Lightweight-Stream-API のいずれの場合も同じ)

Ayako=[Person(id=100, name=Ayako), Person(id=101, name=Ayako)]
Beck=[Person(id=11, name=Beck)]
Hanako=[Person(id=5, name=Hanako)]
Taro=[Person(id=1, name=Taro), Person(id=3, name=Taro)]
Jiro=[Person(id=2, name=Jiro)]


Lightweight-Stream-APIでは提供されていないものもある

Java8標準のStream APIでは提供されているが、Lightweight-Stream-APIでは提供されない操作も存在しています。
例えば、parallelなどは、その代表格と言えるかもしれません


さらなるStream操作

Lightweight-Stream-APIでは既に紹介したStream操作以外にもJava8標準では対応する操作が無いようなものもAPIとして提供されています。
それらについては以下エントリを参照ください。




関連エントリ