Java8 Stream API Tips
Java Stream APIで調べたりして学んだTipsのメモです。
いいTipsがあったら随時追記していこうと思います。
目次
フィールドでdistinct()する
3つのプロパティを持つPersonクラスがあった場合。
Person.java
@Value @AllArgsConstructor private static class Person { String name; String country; int age; }
Personのインスタンス。
private List<Person> persons; private Person annie; private Person bobby; private Person cindy; private Person danny; private Person anny; { annie= new Person("Annie", "America", 42); bobby = new Person("Bobby", "Japan", 34); cindy = new Person("Cindy", "America", 22); danny = new Person("Danny", "Brazil", 22); anny = new Person("Annie", "America", 42); persons = Arrays.asList(annie, bobby, cindy, danny, anny); }
Streamの要素を一意にする場合、こんな感じでdistinct()を使用します。
List<Person> distinct = persons.stream() .distinct() .collect(Collectors.toList());
distinct()はオブジェクトのequals()によって判定されます。
そのため、Personのフィールドの要素(nameなど)で一意にしたくても出来ません。
こんな感じで書けたら便利なのですが、出来ないです。
// エラー // こういう書き方は出来ない List<Person> distinct = persons.stream() .distinct(p -> p.name()) .collect(Collectors.toList());
なんかいい方法がないかな、と探していたら下記に答えがあった。
http://stackoverflow.com/questions/23699371/java-8-distinct-by-property
やり方としてはこんな感じ。
sequential streamの場合
hashMapを利用して、filter()で出現済みかどうかを判定。
Person.nameで一意にする例と、Person.ageで一意にする例。
@Test public void distinctByPropertyTest() { Map<String, Boolean> seenCountry = new HashMap<>(); List<Person> distinctByCountry = persons.stream() .filter(p -> seenCountry.putIfAbsent(p.country, Boolean.TRUE) == null) .collect(Collectors.toList()); assertThat(distinctByCountry).containsExactlyInAnyOrder(annie, bobby, danny); Map<Integer, Boolean> seenAge = new HashMap<>(); List<Person> distinctByAge = persons.stream() .filter(p -> seenAge.putIfAbsent(p.age, Boolean.TRUE) == null) .collect(Collectors.toList()); assertThat(distinctByAge).containsExactlyInAnyOrder(annie, bobby, cindy); }
parallel streamの場合
ConcurrentHashMapを使用してスレッドセーフにして、filter()で出現済みかどうかを判定。
ただし、順序付けされたparallel streamの場合は順番が保証されないので注意が必要です。
またsequential streamでも利用可能ですが、ConcurrentHashMapによるオーバーヘッドがあります。
下記のようにヘルパーメソッドとして切り出すと便利。
private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) { Map<Object,Boolean> seen = new ConcurrentHashMap<>(); return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; }
テスト。
@Test public void distinctByKeyTest() { List<Person> distinctByCountry = persons.stream() .filter(distinctByKey(p -> p.country)) .collect(Collectors.toList()); assertThat(distinctByCountry).containsExactlyInAnyOrder(annie, bobby, danny); List<Person> distinctByAge = persons.stream() .filter(distinctByKey(p -> p.age)) .collect(Collectors.toList()); assertThat(distinctByAge).containsExactlyInAnyOrder(annie, bobby, cindy); }
特定のフィールドでsorted()する
sorted()はComparableを実装している場合、何も指定しないと自然順序に従ってソートされます。
3つのプロパティを持つPersonクラスがある場合。
特定のフィールドの要素(nameなど)でソートしたい場合は、Comparableを実装するか、
Comparatorを引数に取るsorted(Comparator comparator)を使えばいいです。
@Value @FieldDefaults(level = AccessLevel.PRIVATE) private static class Person { String name; String country; int age; } private List<Person> persons = new ArrayList<>(); private Person Anna; private Person Bobby; private Person Bob; private Person David; @Before public void setUp() { Anna = new Person("Anna", "Canada", 24); Bobby = new Person("Bobby", "Brazil", 42); Bob = new Person("Bobby", "America", 30); David = new Person("David", "America", 33); persons = Arrays.asList(Anna, Bobby, Bob, David); }
ラムダ式でComparable.compareTo()を実装してnameの昇順でソート。
@Test public void nameで昇順でsort_compareTo() { List<Person> sorted = persons.stream() .sorted((a, b) -> a.getName().compareTo(b.getName())) .collect(Collectors.toList()); assertThat(sorted).containsSubsequence(Anna, Bob, David); assertThat(sorted).containsSubsequence(Anna, Bobby, David); }
Comparator.comparing()を使用してnameの昇順でソート。
こちらのほうが可読性が高く、降順も書きやすい。
@Test public void nameで昇順でsort_comparing() { List<Person> sorted = persons.stream() .sorted(Comparator.comparing(Person::getName)) .collect(Collectors.toList()); assertThat(sorted).containsSubsequence(Anna, Bob, David); assertThat(sorted).containsSubsequence(Anna, Bobby, David); }
Comparator.comparing()を使用してnameの降順でソート。
reversed()をつける。
@Test public void nameで降順でsort() { List<Person> sorted = persons.stream() .sorted(Comparator.comparing(Person::getName).reversed()) .collect(Collectors.toList()); assertThat(sorted).containsSubsequence(David, Bob, Anna); assertThat(sorted).containsSubsequence(David, Bobby, Anna); }
nameの降順でソートその2。
Comparator.comparing()の第2引数にComparator.reverseOrder()を指定。
@Test public void nameで降順でsort2() { List<Person> sorted = persons.stream() .sorted(Comparator.comparing(Person::getName, Comparator.reverseOrder())) .collect(Collectors.toList()); assertThat(sorted).containsSubsequence(David, Bob, Anna); assertThat(sorted).containsSubsequence(David, Bobby, Anna); }
複数のフィールドでsorted()する
上記と同様のPersonクラスを利用して、
nameの昇順、countryの昇順でソートしたい場合。
Comparable.compareTo()を実装して実現。
@Test public void nameで昇順_countryで昇順でsort() { List<Person> sorted = persons.stream() .sorted(comparatorWithNameAndAge) .collect(Collectors.toList()); assertThat(sorted).containsExactly(Anna, Bob, Bobby, David); } private Comparator<Person> comparatorWithNameAndAge = (p1, p2) -> { int result = p1.getName().compareTo(p2.getName()); if (result != 0) { return result; } return p1.getCountry().compareTo(p2.getCountry()); };
Comparator.thenComparing()で関数合成したほうが可読性も高いし、汎用性が高い気がする。
@Test public void nameで昇順_countryで昇順でsort_関数合成() { List<Person> sorted = persons.stream() .sorted(comparatorWithFunctionSynthesis) .collect(Collectors.toList()); assertThat(sorted).containsExactly(Anna, Bob, Bobby, David); } private Comparator<Person> compareWithName = Comparator.comparing(Person::getName); private Comparator<Person> compareWithCountry = Comparator.comparing(Person::getCountry); // 関数合成 private Comparator<Person> comparatorWithFunctionSynthesis = compareWithName.thenComparing(compareWithCountry);
関数合成を使えば、別のソート順も簡単にできる。
nameの昇順、countryの降順でソートしたい場合。
@Test public void nameで昇順_countryで降順でsort_関数合成() { List<Person> sorted = persons.stream() .sorted(compareWithName.thenComparing(compareWithCountry.reversed())) .collect(Collectors.toList()); assertThat(sorted).containsExactly(Anna, Bobby, Bob, David); }
テストコードは下記に置きました。
Tipsを学んだら随時追記してこうと思います。
終わり。