なみひらブログ

学んだことを日々記録する。~ since 2012/06/24 ~

Java8言語仕様でよく使う書き方のまとめ【Stream、Lambda関連】

背景

最近良く使っているJava8の構文についてまとめておきます。
普段似たような処理を書きたいときに「あれっ?どんな感じで書くんだっけ?(;´Д`)」となるので備忘録的なメモです。

小言

以前はif/for文ぐらいを駆使してゴリゴリ書いていたので言語仕様に振られない範囲で書けましたが、
Streamが出てきて言語仕様レベルから表現が豊かになった(=メソッドの選択肢と組合せが豊富)ので、書くとき結構悩みます(;´Д`)

前提

以下のクラスを利用しています。

  • 特記事項
    • 「nameを引数に取るコンストラクタ」や「public Person setName(...)」があったほうが便利だが、単純なクラスで例を出したほうがいいと思ったので無しにしています。
    • そのため、インスタンス生成に冗長な記載があります。
    • @Setter/@GetterはLombokのアノテーション。今回のメモには関係がないです。
@Setter
@Getter
public class Person{
    private String name;
}

メモ

既存のコレクションについて、型を変換したコレクションが欲しい

例:名前(文字列)一覧から、対応する人インスタンス(Person型)に変換する

  • Java8仕様
    • sequential()は、コレクションの実行順序を保証したStreamを作る。必要とは限らないが今回はテスト結果確認のため付けています。
    • map()は、コレクションの各要素が渡ってきて(n:変数名は任意)、その値を使って処理して任意の型を返していいメソッド
    • collect(Collectors.toList())は、結果のListを作るための定型文。
@Test
public void test_lambda_list(){
    // setup
    final List<String> names = Arrays.asList("namihira", "john", "james");

    // action
    final List<Person> persons = names.stream()
                                      .sequential()
                                      .map(n -> {
                                              final Person p = new Person();
                                              p.setName(n);
                                              return p;
                                       })
                                      .collect(Collectors.toList());

    // check
    assertEquals(names.size(), persons.size());
    assertEquals(names.get(0), persons.get(0).getName());
    assertEquals(names.get(1), persons.get(1).getName());
    assertEquals(names.get(2), persons.get(2).getName());
}
  • ちなみにJava8以前の仕様で書くと
@Test
public void test_no_lambda_list(){
    // setup
    final List<String> names = Arrays.asList("namihira", "john", "james");

    //action
    final List<Person> persons = new ArrayList<>();
    for (String name : names) {
        final Person p = new Person();
        p.setName(name);
        persons.add(p);
    }

    //check
    assertEquals(names.get(0), persons.get(0).getName());
    assertEquals(names.get(1), persons.get(1).getName());
    assertEquals(names.get(2), persons.get(2).getName());
}

既存のコレクションについて、指定のものだけについて型を変換したコレクションが欲しい

例:名前(文字列)一覧から、イニシャルが"j"の人だけインスタンス(Person型)一覧を取得する。

  • Java8仕様
    • filter()は、中にtrue/falseを返す式を書いてtrueになったものだけを後工程に渡す。今回は「イニシャルが"j"であるか」がその条件式。
@Test
public void test_lambda_list_filter(){
    // setup
    final List<String> names = Arrays.asList("namihira", "john", "james");

    // action
    final List<Person> persons = names.stream()
                                      .sequential()
                                      .filter(n -> n.startsWith("j"))
                                      .map(n -> {
                                              final Person p = new Person();
                                              p.setName(n);
                                              return p;
                                            })
                                      .collect(Collectors.toList());

    // check
    assertEquals(names.size() - 1, persons.size());
    assertEquals(names.get(1), persons.get(0).getName());
    assertEquals(names.get(2), persons.get(1).getName());
}
  • ちなみにJava8以前の仕様で書くと
@Test
public void test_no_lambda_list_filter(){
    // setup
    final List<String> names = Arrays.asList("namihira", "john", "james");

    //action
    final List<Person> persons = new ArrayList<>();
    for (String name : names) {
        if (name.startsWith("j")) {
            final Person p = new Person();
            p.setName(name);
            persons.add(p);
        }
    }

    //check
    assertEquals(names.size() - 1, persons.size());
    assertEquals(names.get(1), persons.get(0).getName());
    assertEquals(names.get(2), persons.get(1).getName());
}

既存のコレクションについて型を変換したコレクションが欲しいけど、ソートもしておきたい

例:名前(文字列)一覧からインスタンス(Person型)一覧への変換と名前順にする。

  • Java8仕様
    • sorted()は、中にソート条件(Comparator)を書いてそのソートした結果を後工程に渡す。
      • String::compareToはメソッド参照の書き方。
        • 一番ゴリゴリに書くなら、Comparatorクラスをnewして書く。後述の「Java8以前の記載」参照。
        • ↑しかし、Comparatorクラスはメソッドが1つしかない(compare)ので*1コンパイラが空気を読んで短縮して記載できる(=Lambda式)。
        • ↑ComparatorクラスをnewをLambda式を利用して書くと「sorted( (n1, n2) -> n1.compareTo(n2) )」のように書ける。
        • ↑さらに今回のように「渡ってきた文字列を比較する」自体はコンパイラ的には自明なようなので短縮して記載できる(=メソッド参照)。「StringクラスのcompareToメソッドでいつものアレでお願い」というイメージ*2(´Д`)。
        • で結局以下の「String::compareTo」のような記載になる。
@Test
public void test_lambda_list_sort(){
    // setup
    final List<String> names = Arrays.asList("namihira", "john", "james");

    // action
    final List<Person> persons = names.stream()
                                      .sorted(String::compareTo)
                                      .sequential()
                                      .map(n -> {
                                              final Person p = new Person();
                                              p.setName(n);
                                              return p;
                                            })
                                      .collect(Collectors.toList());

    // check
    assertEquals(names.size(), persons.size());
    assertEquals("james", persons.get(0).getName());
    assertEquals("john", persons.get(1).getName());
    assertEquals("namihira", persons.get(2).getName());
}
  • ちなみにJava8以前の仕様で書くと
    • Lambda式に慣れてくると、「new Comparator() { @Override・・・」あたりがすごく冗長に見えてくる(;´Д`)
@Test
public void test_no_lambda_list_sort(){
    // setup
    final List<String> names = Arrays.asList("namihira", "john", "james");

    // action
    Collections.sort(names, new Comparator<String>() {
        @Override
        public int compare(String s1, String s2) {
            return s1.compareTo(s2);
        }
    });

    final List<Person> persons = new ArrayList<>();
    for (String name : names) {
        final Person p = new Person();
        p.setName(name);
        persons.add(p);
    }

    // check
    assertEquals(names.size(), persons.size());
    assertEquals("james", persons.get(0).getName());
    assertEquals("john", persons.get(1).getName());
    assertEquals("namihira", persons.get(2).getName());
}

既存のコレクションについて、各要素のある値だけをかき集めたい

例:人(Person型)一覧から名前だけを抽出したい。

  • Java8仕様
    • 「Person::getName」もメソッド参照。
      • この場合は、「map(p -> p.getName())」のように書くが、これもコンパイラにとって自明なので短縮して記載できる。「PersonクラスのgetNameメソッド使ってと言えば分かるでしょ」というイメージ*3(´Д`)。
@Test
public void test_lambda_list_method_ref(){
    // setup
    final Set<String> names = new HashSet<>(Arrays.asList("namihira", "john", "james"));
    final List<Person> persons = names.stream()
                                      .sequential()
                                      .map(n -> {
                                              final Person p = new Person();
                                              p.setName(n);
                                              return p;
                                          })
                                      .collect(Collectors.toList());


    // action
    final Set<String> result = persons.stream()
                                      .map(Person::getName)
                                      .collect(Collectors.toSet());

    // check
    assertEquals(names, result);
}
  • ちなみにJava8以前の仕様で書くと
@Test
public void test_no_lambda_list_method_ref(){
    // setup
    final Set<String> names = new HashSet<>(Arrays.asList("namihira", "john", "james"));
    final List<Person> persons = names.stream()
                                      .sequential()
                                      .map(n -> {
                                              final Person p = new Person();
                                              p.setName(n);
                                              return p;
                                          })
                                      .collect(Collectors.toList());


    // action
    final Set<String> result = new HashSet<>();
    for (Person p : persons) {
        result.add(p.getName());
    }

    // check
    assertEquals(names, result);
}

既存のコレクションについて、グループ分けしたい

例:名前一覧について、イニシャルでグルーピングしたい。

  • Java8仕様
    • collect×CollectorsはStream関連でも結構バラエティに富んだ表現があるが、一番使うのがこのパターン。
    • 詳細はJavadoc参照(;´Д`)※基本どういう結果になるのかが読み解けないので、トライ・アンド・エラーで試してみるのが一番だったりする。
@Test
public void test_lambda_list_grouping(){
    // setup
    final List<String> names = Arrays.asList("namihira", "john", "james");

    // action
    final Map<String, List<String>> result = names.stream()
                                                  .collect(Collectors.groupingBy(
                                                                          n -> String.valueOf(n.charAt(0)),
                                                                          Collectors.toList()
                                                                      )
                                                    );

    // check
    assertEquals(2, result.keySet().size());
    assertEquals(1, result.get("n").size());
    assertEquals(2, result.get("j").size());
    assertTrue(result.get("n").contains("namihira"));
    assertTrue(result.get("j").contains("john"));
    assertTrue(result.get("j").contains("james"));
}
  • ちなみにJava8以前の仕様で書くと
@Test
public void test_no_lambda_list_grouping(){
    // setup
    final List<String> names = Arrays.asList("namihira", "john", "james");

    // action
    final Map<String, List<String>> result = new HashMap<>();
    for (String name : names) {
        final String initial = String.valueOf(name.charAt(0));
        List<String> list = result.get(initial);
        if (list == null) {
            list = new ArrayList<>();
        }
        list.add(name);
        result.put(initial, list);
    }

    // check
    assertEquals(2, result.keySet().size());
    assertEquals(1, result.get("n").size());
    assertEquals(2, result.get("j").size());
    assertTrue(result.get("n").contains("namihira"));
    assertTrue(result.get("j").contains("john"));
    assertTrue(result.get("j").contains("james"));
}

まとめ

毎回書いて、毎回忘れる(;´Д`)

参考

*1:実装しないいけないメソッドの意。Java8からインタフェースにもメソッドが実装できるため、メソッド自体は多い

*2:個人的な見解です

*3:個人的な見解です