Stream#filter()で絞り込みmap()で処理するコードスニペット

公開日: : Java ,

Java8 から導入された Stream API も少しずつ慣れてきて、「そういえばこういうパターンはよく使っているな」というものがいくつかあるのでメモしておく。

今回はその2つ目。
1つ目の記事のリンクを貼っておく。

目次

1. List 内のオブジェクトから条件に一致するものに処理を適用して新しい List を生成する

あるクラスのオブジェクトが List に格納されており、条件に一致するものだけに処理を行って別の List を生成したいというパターン。(そのまんまだが)フィルタで「ふるい分け」しつつ、「ついでの処理」も行う場合に使っている。

「この手のコードはこうやって書くんだろう」と以前は思っていたが、Stream API に触れるうちに「実はこうやって書いたほうがそれっぽい」と気付きもあったので、Before と After の例も載せておく。

2. Before: Stream#forEach() を使って書いていたコード

まずは Before のコードから。

例として次のような値を格納する Goods というクラスがある。

public class Goods {

    /**
     * 商品名。
     */
    private final String name;

    /**
     * 製造年月日。
     */
    private final LocalDate dateOfPacking;

    /**
     * 価格。
     */
    private final Integer price;
}

ここに「製造年月日が基準日よりも過去であるか」を問い合わせる isBefore() というメソッドを追加する。このメソッドは「ふるい分け」に使用する。

    /**
     * {@link Goods#dateOfPacking} が基準日付よりも過去であるかを問い合わせる。
     * 
     * @param basis 基準日付。{@code null} は許容しない。
     * @return 基準日付よりも過去の場合 {@code true}。
     */
    public boolean isBefore(LocalDate basis) {
        Objects.requireNonNull(basis, "basis の null は許容しない。必須。");

        return dateOfPacking.isBefore(basis);
    }

また、指定した割引率で値下げした Goods を生成する getDiscountedGoogds() というメソッドも追加する。このメソッドは「ついでの処理」に使用する。

    /**
     * {@link Goods#price} を指定の {@code rate} で割引した {@link Goods} を生成する。
     * 
     * @param rate 割引率。負の値を指定した場合は {@link IllegalArgumentException} を {@code throw} する。
     * @return
     */
    public Goods getDiscountedGoods(int rate) {
        if (rate < 0) {
            throw new IllegalArgumentException(String.format("割引率 %s は無効です。割引率には正の値を指定してください。", rate));
        }

        return new Goods(name, dateOfPacking, discount.applyAsInt(rate));
    }

    /**
     * 割引後の価格を計算する {@link IntUnaryOperator} 型の関数。
     *  
     */
    private final IntUnaryOperator discount = (rate) -> price - (price * rate / 100);

これで「あるクラスのオブジェクトが List に格納されており、条件に一致するものだけに処理を行って別の List を生成したい」の準備が整った。

シナリオは次の通りとする。

  • List に格納している Goods の「製造年月日(=dateOfPacking)」が「基準日付」よりも過去であれば、価格(=price)を10%下げる

さて、Stream#filter()Stream#map() を使っていなかった頃の Before なコードは次の通り。

/**
 * 現在日付から {@code days} の日数を加算した {@link LocalDate} を生成する関数。
 * 
 */
final Function<Long, LocalDate> dateOfPacking = (days) -> LocalDate.now().plusDays(days);

@Test
public void filterメソッドを使って絞り込む_before1() throws Exception {
    // `Goods` を格納した `List`
    List<Goods> goods = Arrays.asList(
    // 1日前の製造年月日
            new Goods("BEFORE_1_DAYS", dateOfPacking.apply(-1L), 100),
            // 1日後の製造年月日
            new Goods("AFTER_1_DAYS", dateOfPacking.apply(1L), 100),
            // 2日前の製造年月日
            new Goods("BEFORE_2_DAYS", dateOfPacking.apply(-2L), 1000));

    // 製造年月日判定の基準日付
    final LocalDate basis = LocalDate.now();

    // 割引率
    final int rate = 10;

    // 条件を満たした `Goods` を格納する `List`
    List<Goods> actual = new ArrayList<>();

    // 基準日付よりも古い製造年月日の商品をピックアップして価格を10%割り引く
    goods.forEach(g -> {
        if (g.isBefore(basis)) {
            actual.add(g.getDiscountedGoods(rate));
        }
    });

    // verify
    assertThat("条件を満たした Goods の個数は 2 である", actual.size(), is(2));
    assertThat("1件目の価格が正しいこと", actual.get(0).getPrice(), is(90));
    assertThat("2件目の価格が正しいこと", actual.get(1).getPrice(), is(900));
}

Stream#filter() を使っていなかった頃は、、、

  • 事前に処理後のオブジェクトを格納する List を生成していた
  • Stream#forEach() でループして、if 文で条件判定していた

というコードをしばらく書いていた。が、もっと違った書き方ができることを知った。

3. After1: Stream#filter() と Stream#map を使って書くようになったコード

その After なコードは次の通り。

@Test
public void filterメソッドを使って絞り込む_after1() throws Exception {
    // `Goods` を格納した `List`
    List<Goods> goods = Arrays.asList(
    // 1日前の製造年月日
            new Goods("BEFORE_1_DAYS", dateOfPacking.apply(-1L), 100),
            // 1日後の製造年月日
            new Goods("AFTER_1_DAYS", dateOfPacking.apply(1L), 100),
            // 2日前の製造年月日
            new Goods("BEFORE_2_DAYS", dateOfPacking.apply(-2L), 1000));

    // 製造年月日判定の基準日付
    final LocalDate basis = LocalDate.now();

    // 割引率
    final int rate = 10;

    // 基準日付よりも古い製造年月日の商品をピックアップして価格を10%割り引く
    List<Goods> actual = goods.stream().filter(g -> g.isBefore(basis)).map(g -> g.getDiscountedGoods(rate)).collect(Collectors.toList());

    // verify
    assertThat("条件を満たした Goods の個数は 2 である", actual.size(), is(2));
    assertThat("1件目の価格が正しいこと", actual.get(0).getPrice(), is(90));
    assertThat("2件目の価格が正しいこと", actual.get(1).getPrice(), is(900));
}

Stream#filter() を使って処理フローが次のように変更される。

  1. Stream#filter() によって「ふるい分け」された Goods を格納した Stream が生成される
  2. Stream#map() によって「ついでの処理」で生成された Goods を格納した Stream が生成される
  3. Stream#collect() によって StreamList に変換する

Java8 っぽい感じになってきた。
filter() して map() して collect() する、というスニペットはよく使っているので覚えておくとよいかもしれない。

4. 更新履歴

  • 2015/01/17
    • 初版作成

5. この記事の内容について

この記事は Javaプログラマーなら習得しておきたい Java SE 8 実践プログラミング を読みながら個人的にまとめたことをメモした内容になっています。

調べ物から記事作成までの全て作業はプライベートな時間に行ったものであり、所属組織のリソースは消費していません。また、記事のアイデアも所属組織に由来していません。

記事の内容が必ずしも正しかったり、最新ではないおそれがあります。参考にされる分には構いませんが、記事の内容を取り入れたことによって発生した損害などの責任は負いません。ご利用は自己判断でお願いいたします。

Javaプログラマーなら習得しておきたい Java SE 8 実践プログラミング

  • 著者Cay S. Horstmann
  • 価格¥ 3,024(2014/10/17 時点)
  • 出版日2014-09-22
  • 商品ランキング24027 位
  • 大型本264 ページ
  • ISBN-104844336673
  • 出版社インプレス

Googleアドセンス用(PC)

  • このエントリーをはてなブックマークに追加
  • follow us in feedly

関連記事

no image

Functions#compose() を使って Function を合成するサンプル(Java8 との比較付き)

Java8 と GoogleGuava の Function について コードの違いを書いた。 その

記事を読む

medium_182531101

ClassLoader を使って getResourceAsStream で取得したファイルを UTF-8 で読み込む

よく使う割には覚えられず毎回調べている気がするので備忘録としてメモしておきます。 文字コードを指定し

記事を読む

no image

画像ファイルのサイズ(縦横じゃなくて容量)を取得するスニペット

File#length で取得できるとは知らなかった。Gist にも登録済み。 /** * 画

記事を読む

no image

サーブレットでクラスパス上にあるプロパティファイルを参照するスニペット

よく使うわりにはいつも調べているのでいい加減メモすることにした。 "app.proper

記事を読む

no image

ディレクトリに関するユーティリティ

ディレクトリに関するスニペットをいくつか書いた。いつかコピペする日が来ると思うのでメモしておく。

記事を読む

no image

Spark を使って JSON を返すシンプルな API を書いてみた

必要に迫られてモックの API を作成することになった。 大した工数を割けないので使ったことのある

記事を読む

medium_2055608272

指定した年月の日付を Calendar オブジェクトの一覧で取得する

小ワザです。 業務で、指定した年月の日付を Calendar オブジェクトの一覧で取得する必要があり

記事を読む

GradleLogoReg

SDKMAN!(GVM) を使って Mac に Gradle をインストールする

長らく Java のアプリ開発には Maven を使ってきたが、時代に取り残されている感じがするので

記事を読む

no image

Stream#map()とStream#collect()を使ってListを生成する

Java8 から導入された Stream API も少しずつ慣れてきて、「そういえばこういうパターン

記事を読む

no image

ファイルに関するユーティリティ

ファイルに関するスニペットをいくつか書いた。いつかコピペする日が来ると思うのでメモしておく。 作成

記事を読む

Googleアドセンス用(PC)

Message

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です


8 − = 一

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Googleアドセンス用(PC)

icatch-jersey_multi_pathparams
Jerseyの@PathParamはスラッシュの間に複数指定できる

http://hoge-api/user/{id}.{format}

icatch-vagrant_box_customize
VagrantのBoxファイルをカスタマイズして独自のBoxファイルを作成する

配布されている Vagrant の Box ファイルを使って検証環境を

icatch-2015-006-1
バリデーションチェックにJava8のOptionalを使ってスマートに書く(自分比)

Web アプリのバリデーションチェックにアノテーションを使うことが増え

icatch-2015-005-1
ユニットテストの偏りを防ぐ命名規則の付け方

ユニットテスト名に以下の命名規則を付けるようにして二ヶ月ぐらい経った。

icatch-2015-004-1
Vagrantで起動したCentOS上のOctopressをホストOSから確認する設定

タイトルの通りだが、Vagrant を使って起動した CentOS に

→もっと見る

PAGE TOP ↑