LoginSignup
0
1

More than 1 year has passed since last update.

【Java】CollectorsAPI 1000本ノック

Last updated at Posted at 2023-01-22

ソースコード

toCollection

        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を使ったほうがいい。

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

toSet

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

Stream -> Setに変換する。

toUnmodifiableSet

        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

        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

        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

        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

        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

        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

        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

        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

        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

        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

        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)

        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)

        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

        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

        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

        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

        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重複時の挙動をきちんと定義しておく必要がある。

toUnmodifiableMap

        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

        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

    @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;
    }

参考:

summarizingInt

        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に入れ込む。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1