読者です 読者をやめる 読者になる 読者になる

Java8 Stream API Tips

Java 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);
    }

テストコードは下記に置きました。

github.com

Tipsを学んだら随時追記してこうと思います。

終わり。