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

公開日: : Java ,

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

今回はその1つ目。

目次

1. List に格納されているオブジェクトから必要なものだけをピックアップして新しい List を生成するというパターン

あるクラスのオブジェクトが List に格納されていて、そのオブジェクトの特定のフィールドだけをピックアップした List を生成したいというパターン。

検索結果として取得した Entity から特定のフィールドを表示用のクラスにコピーして List に格納したい場合によく使っている。

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

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

まずは Before のコードから。

例として次のような Person というクラスがある。

public class Person {

    /**
     * 名前。
     */
    private final String name;

    /**
     * 年齢。
     */
    private final int age;

    /**
     * 性別。
     */
    private final Gender gender;

    // Getter, Setter, コンストラクタは省略。
}

また、Person クラスの name だけを格納する PersonsName というクラスもあったとする。

public class PersonsName {

    /**
     * 名前。
     */
    private final String name;

    // Getter, Setter, コンストラクタは省略。
}

Person が複数格納されている List があった場合に、それを元に PersonsName が格納される List を生成するには次のように書くものと思っていた。

@Test
public void mapメソッドを使ってListを生成する_before1() throws Exception {

    // `Person` を格納した `List`
    List<Person> persons = Arrays.asList(
            // 1つ目の `Person`
            new Person("NAME_1", 40, Gender.MALE),
            // 2つ目の `Person`
            new Person("NAME_2", 30, Gender.FEMALE),
            // 3つ目の `Person`
            new Person("NAME_3", 20, Gender.MALE));

    // `PersonsName` を格納する `List`
    //   `Stream#forEach()` 内で生成したオブジェクトを格納する `List` は事前に用意しておかないとダメなんだと勘違いしていた
    List<PersonsName> actual = new ArrayList<>();
    
    // `Stream#forEach()` を使って、`Person` から `PersonsName` を生成して ↑ で生成した `List` に詰める
    persons.stream().forEach(person -> {
        PersonsName personsName = new PersonsName(person.getName());
        actual.add(personsName);
    });

    // verify
    assertThat("生成された List のサイズも 3 である", actual.size(), is(3));
    assertThat("先頭の名前が正しいこと", actual.get(0).getName(), is("NAME_1"));
    assertThat("最後の名前が正しいこと", actual.get(actual.size() - 1).getName(), is("NAME_3"));
}

コメントにも書いたが、Stream#forEach() の中では List を作れないので、Stream#forEach() を実行する前に PersonsName を格納する List は生成しておかなければいけないと思っていた。
また、従来の for 文が Stream#forEach() になっただけで見た目にもそれっぽくない。

でも、「この手のコードはこうやって書くんだろう」と勘違いしていて、しばらくはこうやって書いていた。が、もっと違った書き方ができることを知った。

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

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

@Test
public void mapメソッドを使ってListを生成する_after1() throws Exception {
    // setup
    List<Person> persons = Arrays.asList(
            // 1つ目の `Person` オブジェクト
            new Person("NAME_1", 40, Gender.MALE),
            // 2つ目の `Person` オブジェクト
            new Person("NAME_2", 30, Gender.FEMALE),
            // 3つ目の `Person` オブジェクト
            new Person("NAME_3", 20, Gender.MALE));

    // `Stream#map()` を使って `Person` から `PersonsName` を生成し、`Stream#collect()` を使って処理結果を `List` に詰める
    List<PersonsName> actual = persons.stream().map(person -> new PersonsName(person.getName())).collect(Collectors.toList());

    // verify
    assertThat("生成された List のサイズも 3 である", actual.size(), is(3));
    assertThat("先頭の名前が正しいこと", actual.get(0).getName(), is("NAME_1"));
    assertThat("最後の名前が正しいこと", actual.get(actual.size() - 1).getName(), is("NAME_3"));
}

Stream クラスのメソッドは『中間処理』と『終端処理』の2種類に分けられる」ということが徐々に理解できるようになってきて ↑ のように書けることに気づいた。

その「理解」とは、中間処理で処理してその結果を終端処理で生成するという流れのこと。
上記は Stream#map() で処理を行い Stream#collect() でその結果を生成している。結果は Collectors.toList() によって List に格納されていくという流れになっている。

何となく Java8 っぽい見た目になってきた。

4. After2: Stream#map() で行う処理を Function 型の変数に切り出す

Stream#map()Person から PersonsName を生成しているが、この処理が他でも使われるようなら関数型インタフェースとして Function 型の変数として切り出すこともできる。

@Test
public void mapメソッドを使ってListを生成する_after2() throws Exception {
    // `Person` を格納した `List`
    List<Person> persons = Arrays.asList(
            // 1つ目の `Person`
            new Person("NAME_1", 40, Gender.MALE),
            // 2つ目の `Person`
            new Person("NAME_2", 30, Gender.FEMALE),
            // 3つ目の `Person`
            new Person("NAME_3", 20, Gender.MALE));

    // `Person` クラスから `PersonsName` を生成する `Function`
    Function<Person, PersonsName> personToPersonsName = (person -> new PersonsName(person.getName()));

    // `Stream#map()` を使って `Person` から `PersonsName` を生成し、`Stream#collect()` を使って処理結果を `List` に詰める
    List<PersonsName> actual = persons.stream().map(personToPersonsName).collect(Collectors.toList());

    // verify
    assertThat("生成された List のサイズも 3 である", actual.size(), is(3));
    assertThat("先頭の名前が正しいこと", actual.get(0).getName(), is("NAME_1"));
    assertThat("最後の名前が正しいこと", actual.get(actual.size() - 1).getName(), is("NAME_3"));
}

何となくだがさらに Java8 っぽい見た目になった気がする。

Function 型の変数として切り出すかは要求される内容によって判断すればいいとして、この「Stream#map() で処理をして Stream#collect() で収集する」というスニペットはよく使っているので覚えておくとよいかもしれない。

5. 更新履歴

  • 2015/01/12
    • 初版作成

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

この記事は 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

google-gson でシリアライズ・デシリアライズしたサンプル

これまで Java で JSON を扱うときは JSONIC を使っていたが、google-gson

記事を読む

no image

Jetty でシンプルなプロジェクトを作成してみた

プロトタイプを作成するなら Tomcat よりも Jetty のほうがさらっと出来そうだなぁ、と今さ

記事を読む

icatch_4610898091-resized

Java8 の Optional とガード節

メソッドの冒頭で条件チェックを行い、条件を満たしていない場合はさっさと return してしまう「ガ

記事を読む

no image

プロセスの起動を確認するプログラム

プロセスが起動しているかを確認するプログラムを Java で書いてみました。Excel のプロセス確

記事を読む

icatch_2185253176-resized

Java8 の Optional 型を返す Generics を使ったメソッドを定義する

Java8 の Optional とガード節 に続いて Java8 の Optional ネタ。今回

記事を読む

no image

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

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

記事を読む

no image

ファイルの拡張子が JPG であるかを問い合わせるスニペット

特に目新しいこともないが、同じようなことをする機会があるかもしれないのでメモ。Gist にも登録済み

記事を読む

no image

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

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

記事を読む

no image

Exec Maven Plugin で maven コマンドでアプリを起動する

packaging タグに "jar" を指定した Maven プロジェクトをコマンドラインから起動

記事を読む

no image

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

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

記事を読む

Googleアドセンス用(PC)

Message

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


八 − 3 =

次の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 ↑