Some_user_qwerty
Some_user_qwerty

Reputation: 341

Perform flatMap operation with Collectors

I would like to understand a way to perform a flatMap while using Collectors. Here is an example.

Scenario:

I have the following interfaces:

interface Ic {
    //empty
}

interface Ib {
    Stream<Ic> getCs();
}

interface Ia {
    String getName();
    Stream<Ib> getBs();
}

And I'm trying to implement the following method:

Map<String, Long> total_of_C_per_A (Stream<Ia> streamOfA) {
   return streamOfA.collect(groupBy(Ia::getName, ???));
}

The classification function is pretty straitforward, my problem is with the downstream collector. I need to count the number of "C" associated with "A".

What I tried to to:

If I wanted to simply return the count, without creating a map, I would do:

streamOfA
  .flatMap(Ia::getBs)
  .flatMap(Ib::getCs)
  .count();

But the Collectors class only allows me to do mapping operations. What else can I try to do?

Thanks.

Upvotes: 5

Views: 762

Answers (2)

holi-java
holi-java

Reputation: 30686

the documentation described the Collectors#mapping as:

Adapts a Collector accepting elements of type U to one accepting elements of type T by applying a mapping function to each input element before accumulation.

The mapping() collectors are most useful when used in a multi-level reduction, such as downstream of a groupingBy or partitioningBy.

which means you can composing any possible Collectors as you can.

import static java.util.stream.Collectors.*;

Map<String, Long> total_of_C_per_A(Stream<Ia> streamOfA) {
    return streamOfA.collect(groupingBy(
            Ia::getName,
            mapping(
                    Ia::getBs,
                    mapping(
                            it -> it.flatMap(Ib::getCs),
            //    reduce() does boxing & unboxing ---v
                            mapping(Stream::count, reducing(0L,Long::sum))
                    )
            )
    ));
}

OR using Collectors#summingLong instead.

Map<String, Long> total_of_C_per_A(Stream<Ia> streamOfA) {
    return streamOfA.collect(groupingBy(
            Ia::getName,
            mapping(
                    Ia::getBs,
                    mapping(
                            it -> it.flatMap(Ib::getCs),
            //    summingLong() does boxing      ---v
                            mapping(Stream::count, summingLong(Long::longValue))
            //      Long::longValue does unboxing operation ---^
                    )
            )
    ));
}

thanks for @Holger point out the potential problem of the code above, that you can simply using summingLong(Stream::count) instead. in this approach is no need to boxing Stream#count which return a long to a Long. and Long::longValue unboxing a Long to long.

Map<String, Long> total_of_C_per_A(Stream<Ia> streamOfA) {
    return streamOfA.collect(groupingBy(
        Ia::getName,
        mapping(
            Ia::getBs,
        //    summingLong() doesn't any boxing ---v
            mapping(it -> it.flatMap(Ib::getCs), summingLong(Stream::count))
        )
    ));
}

Upvotes: 5

Holger
Holger

Reputation: 298153

This answer points you already into the right direction, but there is no need to nest multiple mapping collectors, as you can just write these functions into a single lambda expression. Considering that the summingLong collector expects a function which evaluates to long, you can simply pass that function to the collector without any mapping collector at all:

Map<String, Long> total_of_C_per_A (Stream<Ia> streamOfA) {
    return streamOfA.collect(groupingBy(
            Ia::getName,
            summingLong(ia -> ia.getBs().flatMap(Ib::getCs).count())));
}

This also has the advantage that the long values are not boxed to Long instances.

There’s also an alternative to flatMap:

Map<String, Long> total_of_C_per_A (Stream<Ia> streamOfA) {
    return streamOfA.collect(groupingBy(
            Ia::getName,
            summingLong(ia -> ia.getBs().mapToLong(ib -> ib.getCs().count()).sum())));
}

Upvotes: 6

Related Questions