【Java】CollectorsAPI 1000本ノック

# ソースコード
https://github.com/tttol/collectors-api-sample

# toCollection
```java
        ArrayList<String> list = Stream.of("a", "b", "c").collect(Collectors.toCollection(ArrayList::new));
        LinkedList<String> linkedList = Stream.of("a", "b", "c").collect(Collectors.toCollection(LinkedList::new));
```
Stream型を任意のListに変換する。
Java17からは、`Stream.toList`を使ったほうがいい。

```java
        List<String> list = Stream.of("a", "b", "c").toList();
```

# toSet
```java
        Set<String> set = Stream.of("a", "b", "c").collect(Collectors.toSet());
```

Stream -> Setに変換する。

# toUnmodifiableSet
```java
        Set<String> set = Stream.of("a", "b", "c").collect(Collectors.toSet());
        set.add("d"); //OK
        Set<String> unmodifiableSet = Stream.of("a", "b", "c").collect(Collectors.toUnmodifiableSet());
        unmodifiableSet.add("d"); //UnsupportedOperationException
```

名前の通り、変更不可のSetを生成する。
`add`自体はできるが、`UnsupportedOperationException`が発生する。

# joining
```java
        var s1 = Stream.of("a", "b", "c");
        var s2 = Stream.of("a", "b", "c");
        var s3 = Stream.of("a", "b", "c");
        var j1 = s1.collect(Collectors.joining());
        var j2 = s2.collect(Collectors.joining(","));
        var j3 = s3.collect(Collectors.joining(",", "[", "]"));

        System.out.println(j1); //abc
        System.out.println(j2); //a,b,c
        System.out.println(j3); //[a,b,c]
```

Stream -> Stringにjoinする。
delimiter, prefix, suffixもオプションで設定可能。

# mapping
```java
        record Person(String name, int age, String sex) {}

        var persons = List.of(
                new Person("Tom", 20, "Male"),
                new Person("John", 22, "Male"),
                new Person("Jessy", 20, "Female"),
                new Person("Hanako", 30, "Female")
        );

        Map<String, List<String>> mappedToList = persons.stream()
                .collect(
                        Collectors.groupingBy(
                                Person::sex,
                                Collectors.mapping(Person::name, Collectors.toList())
                        )
                );
        mappedToList.forEach((key, value) -> System.out.printf("%s=%s%n", key, value));
        //Female=[Jessy, Hanako]
        //Male=[Tom, John]

        Map<String, String> mappedToString = persons.stream()
                .collect(
                        Collectors.groupingBy(
                                Person::sex,
                                Collectors.mapping(Person::name, Collectors.joining("|"))
                        )
                );
        mappedToString.forEach((key, value) -> System.out.printf("%s=%s%n", key, value));
        //Female=Jessy|Hanako
        //Male=Tom|John

```

`Collectors.groupingBy`した結果のvalueの方を任意の形にマッピングする。

# flatMapping
```java
        record Product (String name, int value) {}
        record Selling (String clientName, List<Product> products){}

        var sellings = List.of(
                new Selling("clientA", List.of(
                        new Product("p1-1", 11),
                        new Product("p1-2", 12),
                        new Product("p1-3", 13)
                    )
                ),
                new Selling("clientB", List.of(
                        new Product("p2-1", 21),
                        new Product("p2-2", 22),
                        new Product("p2-3", 23),
                        new Product("p2-4", 24)
                    )
                ),
                new Selling("clientC", List.of(
                        new Product("p3-1", 31),
                        new Product("p3-2", 32)
                    )
                )
        );
        Map<String, List<Product>> flatMapped = sellings.stream()
                .collect(Collectors.groupingBy(Selling::clientName,
                        Collectors.flatMapping(selling -> selling.products.stream(), Collectors.toList())));
        flatMapped.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));

        Map<String, List<List<Product>>> mapped = sellings.stream()
                .collect(Collectors.groupingBy(Selling::clientName,
                        Collectors.mapping(Selling::products, Collectors.toList())));
        mapped.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
        //clientC=[Product[name=p3-1, value=31], Product[name=p3-2, value=32]]
        //clientB=[Product[name=p2-1, value=21], Product[name=p2-2, value=22], Product[name=p2-3, value=23], Product[name=p2-4, value=24]]
        //clientA=[Product[name=p1-1, value=11], Product[name=p1-2, value=12], Product[name=p1-3, value=13]]

```
`Collectors.groupingBy`した結果がListのネストになりうる場合に、flatなListにできる。
参考：https://retheviper.github.io/posts/java-new-methods-from-9-to-11/#flatmapping-9

# filtering
```java
        record Employee (String name, String department, int salary) {}

        var employees = List.of(
                new Employee("Tom", "d1", 10000),
                new Employee("John", "d2", 20000),
                new Employee("Michael", "d2", 20000),
                new Employee("Bob", "d3", 30000)
        );

        Map<String, List<Employee>> filtered = employees.stream().collect(Collectors.groupingBy(
                Employee::department, Collectors.filtering(e -> e.salary >= 20000, Collectors.toList())));
        filtered.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
//        d1=[]
//        d2=[Employee[name=John, department=d2, salary=20000], Employee[name=Michael, department=d2, salary=20000]]
//        d3=[Employee[name=Bob, department=d3, salary=30000]]
```
groupingByした結果をfilterできる。

# collectingAndThen
```java
        List<String> list = Stream.of("a", "b", "c").collect(
                Collectors.collectingAndThen(Collectors.toList(),
                        e -> e.stream().map(String::toUpperCase).toList())
        );
        list.forEach(System.out::println);
//        A
//        B
//        C
```

collectした結果に対して更に操作を加えることができる。

# counting
```java
        var count = Stream.of("a", "b", "c").collect(Collectors.counting());
        System.out.println(count);
        // 3
```

Streamの個数を返す。
`var count = (Long) Stream.of("a", "b", "c").count();`でいい。

# minBy
```java
        record Entity(String key, String seq) {}
        var entities = List.of(
                new Entity("k1", "0001"),
                new Entity("k1", "0003"),
                new Entity("k1", "0002"),
                new Entity("k2", "0002"),
                new Entity("k2", "0001")
        );
        
        Map<String, Optional<Entity>> map = entities.stream()
                .collect(Collectors.groupingBy(Entity::key, Collectors.minBy(Comparator.comparing(Entity::seq))));
                map.forEach((key, value) -> System.out.printf("%s=%s%n", key, value));
```

collect結果から最小値だけを取り出す

# maxBy
```java
        record Entity(String key, String seq) {}
        var entities = List.of(
                new Entity("k1", "0001"),
                new Entity("k1", "0003"),
                new Entity("k1", "0002"),
                new Entity("k2", "0002"),
                new Entity("k2", "0001")
        );

        Map<String, Optional<Entity>> map = entities.stream()
                .collect(Collectors.groupingBy(Entity::key, Collectors.maxBy(Comparator.comparing(Entity::seq))));
        map.forEach((key, value) -> System.out.printf("%s=%s%n", key, value));
```

collect結果から最小値だけを取り出す

# summingInt
```java
        var sum = Stream.of(1, 2, 3).collect(Collectors.summingInt(Integer::intValue));
        System.out.println(sum);
        // 6
```

合計値をIntで返す。
`var sum = (Integer) Stream.of(1, 2, 3).mapToInt(Integer::intValue).sum();`でええやろ、とIDEから警告が出るので、あまり使うことはない？

# averagingInt
```java
        var ave = Stream.of(1, 2, 3).collect(Collectors.averagingInt(Integer::intValue));
        System.out.println(ave);
        // 2.0
```

平均値を出す。返り値は`Double`

# reducing
## `reducing(T identity, BinaryOperator<T> op)`
TBD

## `reducing(BinaryOperator<T> op)`
```java
        record Person(String name, String city, int height){}

        var people = List.of(
                new Person("Tom", "NY", 170),
                new Person("John", "NY", 180),
                new Person("Bob", "LA", 175),
                new Person("Michael", "LA", 172)
        );

        var byHeight = Comparator.comparing(Person::height);
        Map<String, Optional<Person>> tallestByCity = people.stream().collect(
                Collectors.groupingBy(Person::city,
                        Collectors.reducing(BinaryOperator.maxBy(byHeight))));
        tallestByCity.forEach((k, v) -> System.out.printf("%s=%s", k, v));
        // LA=Optional[Person[name=Bob, city=LA, height=175]]NY=Optional[Person[name=John, city=NY, height=180]]
```

引数の`BinaryOperator<T> op`に畳み込み関数を渡すことで、groupingByした結果を畳み込む。
Javadocに以下の記載がある。
> API Note:
The reducing() collectors are most useful when used in a multi-level reduction, downstream of groupingBy or partitioningBy. To perform a simple reduction on a stream, use Stream.reduce(BinaryOperator) instead.

シンプルなreduceならStream APIのreduceを使ったほうがいいよ、とのこと。

## `reducing(U identity, Function<? super T,? extends U> mapper, BinaryOperator<U> op)`
```java
        var byLength = Comparator.comparing(String::length);
        Map<String, String> longestNameByCity = people.stream().collect(
                Collectors.groupingBy(Person::city,
                        Collectors.reducing("",
                                Person::name,
                                BinaryOperator.maxBy(byLength))));
        longestNameByCity.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
        // LA=Michael
        // NY=John
```

最後のMapのvalueの型をStringにできる。

# groupingBy
```java
        record Person(String name, String city, int height){}

        var people = List.of(
                new Person("Tom", "NY", 170),
                new Person("John", "NY", 180),
                new Person("Bob", "LA", 175),
                new Person("Michael", "LA", 172)
        );

        Map<String, List<Person>> tallestByCity = people.stream().collect(Collectors.groupingBy(Person::city));
        tallestByCity.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
        // LA=[Person[name=Bob, city=LA, height=175], Person[name=Michael, city=LA, height=172]]
        // NY=[Person[name=Tom, city=NY, height=170], Person[name=John, city=NY, height=180]]
```

オブジェクトのリストに対して、オブジェクトの任意のプロパティでGROUP BYする。

返り値が`Map<String, List<Person>>`とやや扱いづらいので、必要に応じて前述の`mapping`, `flatMapping`などで任意の型に整形する。

`groupingBy`した結果に更に処理を加えたい場合は`maxBy`などで処理する。

# groupingByConcurrent
```java
        record Person(String name, String city, int height){}

        var people = List.of(
                new Person("Tom", "NY", 170),
                new Person("John", "NY", 180),
                new Person("Bob", "LA", 175),
                new Person("Michael", "LA", 172)
        );

        ConcurrentMap<String, List<Person>> groupingBy1 = people.stream().collect(Collectors.groupingByConcurrent(Person::city));
        groupingBy1.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
        // LA=[Person[name=Bob, city=LA, height=175], Person[name=Michael, city=LA, height=172]]
        // NY=[Person[name=Tom, city=NY, height=170], Person[name=John, city=NY, height=180]]
```
ConcurrentMapを返すgroupingBy

# partitioningBy
```java
        record Person(String name, String city, int height){}

        var people = List.of(
                new Person("Tom", "NY", 170),
                new Person("John", "NY", 180),
                new Person("Bob", "LA", 175),
                new Person("Michael", "LA", 172)
        );

        Map<Boolean, List<Person>> partitioningBy1 = people.stream()
                .collect(Collectors.partitioningBy(person -> person.height >= 175));
        partitioningBy1.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
        // false=[Person[name=Tom, city=NY, height=170], Person[name=Michael, city=LA, height=172]]
        // true=[Person[name=John, city=NY, height=180], Person[name=Bob, city=LA, height=175]]

        Map<Boolean, Long> partitioningBy2 = people.stream()
                .collect(Collectors.partitioningBy(person -> person.height >= 175, Collectors.counting()));
        partitioningBy2.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
        // false=2
        // true=2
```

streamに対してPredicateによるfilteringを実行し`Map<Boolean, List<Person>>`で返す。
第2引数を指定することで、返り値を少し使いやすくもできる。

# toMap
```java
        record Person(String name, String city, int height){}

        var people = List.of(
                new Person("Tom", "NY", 170),
                new Person("John", "NY", 180),
                new Person("Bob", "LA", 175),
                new Person("Michael", "LA", 172)
        );

        Map<String, String> map = people.stream().collect(Collectors.toMap(
                Person::city,
                Person::name,
                (oldValue, newValue) -> newValue // key重複時は後勝ちでPUT
        ));
        map.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
        // LA=Michael
        // NY=John
```

streamをmapにする。
第3引数でkey重複時の挙動をきちんと定義しておく必要がある。

https://blog1.mammb.com/entry/2017/04/21/122919

# toUnmodifiableMap
```java
        record Person(String name, String city, int height){}

        var people = List.of(
                new Person("Tom", "NY", 170),
                new Person("John", "NY", 180),
                new Person("Bob", "LA", 175),
                new Person("Michael", "LA", 172)
        );

        Map<String, String> map = people.stream().collect(Collectors.toUnmodifiableMap(
                Person::city,
                Person::name,
                (oldValue, newValue) -> newValue // key重複時は後勝ちでPUT
        ));
        map.put("hoge", "fuga"); //UnsupportedOperationException
```

immutableなmapを生成する。
putしようとすると、UnsupportedOperationExceptionが発生する。

# toConcurrentMap
```java
        record Person(String name, String city, int height){}

        var people = List.of(
                new Person("Tom", "NY", 170),
                new Person("John", "NY", 180),
                new Person("Bob", "LA", 175),
                new Person("Michael", "LA", 172)
        );

        ConcurrentMap<String, String> map = people.stream().collect(Collectors.toConcurrentMap(
                Person::city,
                Person::name,
                (oldValue, newValue) -> newValue // key重複時は後勝ちでPUT
        ));
        map.forEach((k, v) -> System.out.printf("%s=%s%n", k, v));
        // LA=Michael
        // NY=John
```

streamからConcurrentMap（スレッドセーフなMap）を生成する。

## 【番外編】concurrentMap
```java
    @Test
    @DisplayName("Mapの場合")
    void mapTest() throws Exception {
        var map = new HashMap<String, Integer>();
        var sumList = parallelSum100(map);

        assertNotEquals(1, sumList
                .stream()
                .distinct()
                .count());
        var wrongResultCount = sumList
                .stream()
                .filter(num -> num != 100)
                .count();

        assertTrue(wrongResultCount > 0);

    }
    @Test
    @DisplayName("ConcurrentHashMapの場合")
    void concurrentMapTest() throws Exception {
        var map = new ConcurrentHashMap<String, Integer>();
        var sumList = parallelSum100(map);

        assertEquals(1, sumList
                .stream()
                .distinct()
                .count());
        var wrongResultCount = sumList
                .stream()
                .filter(num -> num != 100)
                .count();

        assertEquals(0, wrongResultCount);
    }

    private List<Integer> parallelSum100(Map<String, Integer> map) throws InterruptedException {
        List<Integer> sumList = new ArrayList<>(1000);
        for (int i = 0; i < 100; i++) {
            map.put("test", 0);
            var executorService = Executors.newFixedThreadPool(4);
            for (int j = 0; j < 10; j++) {
                executorService.execute(() -> {
                    for (int k = 0; k < 10; k++)
                        map.computeIfPresent(
                                "test",
                                (key, value) -> value + 1
                        );
                });
            }
            executorService.shutdown();
            executorService.awaitTermination(5, TimeUnit.SECONDS);
            sumList.add(map.get("test"));
        }
        return sumList;
    }
```

参考：

https://www.baeldung.com/java-concurrent-map

# summarizingInt
```java
        IntSummaryStatistics intSummaryStatistics = Stream.of(1, 2, 3).collect(Collectors.summarizingInt(Integer::intValue));
        System.out.println(intSummaryStatistics);
        // IntSummaryStatistics{count=3, sum=6, min=1, average=2.000000, max=3}
```

streamをIntSummaryStatisticsに入れ込む。