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

関連記事

icatch-10139609144_5bc33706f9_z-resized

保存ダイアログを表示させてファイルをダウンロードさせる方法

例えば、ブラウザに表示している画像は右クリックを使ってダウンロードできますが、左クリックから保存ダイ

記事を読む

icatch-thumbnail

レスポンスにサムネイル画像を返す API のサンプル

今回は直接 ImageMagick は関係ありませんが、また画像ネタです。この辺りのことを仕事で扱っ

記事を読む

no image

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

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

記事を読む

GradleLogoReg

Gradle で Java アプリのプロジェクトを作成して Eclipse にインポートするまでの手順

Mac に SDKMAN! を使って Gradle をインストールしたので、実際にプロジェクトを作成

記事を読む

no image

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

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

記事を読む

icatch-zip_524574337_mini-thumbnail

Java でファイルが1つだけ圧縮された zip を解凍するサンプル

ファイルが1つだけ圧縮された zip を解凍するサンプルです。 単一ファイルが圧縮された zip

記事を読む

no image

Java から VBScript を使って Excel を印刷する

背景 Java のデスクトップアプリを作成していたときの要件に「Excel で作成された帳票を印刷

記事を読む

no image

画像ファイルを選別するFilenameFilter

画像ファイルのみアップロードを許可する仕様があって、何てことはないがフィルターを作って対応したのでメ

記事を読む

no image

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

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

記事を読む

no image

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

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

記事を読む

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 ↑