5

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.

holi-java
  • 29,655
  • 7
  • 72
  • 83
Some_user_qwerty
  • 341
  • 1
  • 3
  • 10
  • This is Java...? – cs95 Jun 25 '17 at 18:49
  • 2
    well there will be a `flatMapping` Collector, but in java-9. Until then... https://stackoverflow.com/questions/41878646/flat-mapping-collector-for-property-of-a-class-using-groupingby – Eugene Jun 25 '17 at 18:50
  • 2
    Nice effort to produce a minimal example, thanks. Would be even easier to understand (and help) if you could give us compilable code at the same time? – Ole V.V. Jun 25 '17 at 19:11
  • You `Map` is declared to have `String` keys. Which strings should go in there? – Ole V.V. Jun 25 '17 at 19:13
  • Sorry for not saying, this is java 8 @Coldspeed – Some_user_qwerty Jun 25 '17 at 22:54
  • Thanks for the link @Eugene, it's nice that java 9 might come with this. – Some_user_qwerty Jun 25 '17 at 22:56
  • I'll take you advice for future questions @Ole V.V. , thanks for it! – Some_user_qwerty Jun 25 '17 at 23:00
  • Eclipse Collections has a `Collector` on the `Collectors2` class called `flatCollect` - https://www.eclipse.org/collections/javadoc/8.1.0/org/eclipse/collections/impl/collector/Collectors2.html#flatCollect-org.eclipse.collections.api.block.function.Function-java.util.function.Supplier- – Donald Raab Jun 26 '17 at 03:06

2 Answers2

6

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())));
}
Holger
  • 285,553
  • 42
  • 434
  • 765
5

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))
        )
    ));
}
Community
  • 1
  • 1
holi-java
  • 29,655
  • 7
  • 72
  • 83
  • Thanks for the answer @holi-java! I studying the specifics of Java 8 and your answer helped me a lot! – Some_user_qwerty Jun 25 '17 at 22:59
  • 2
    @Eugene: compared to what? `Collectors.counting()` doesn’t work here, and `reducing(0L,Long::sum)` will still be boxing under Java 9. Note that in this answer, the potential to reduce without boxing hasn’t been exploited anyway, as `mapping(Stream::count,summingLong(Long::longValue))` still boxes all `long` values without any need; it could be simply written as `summingLong(Stream::count)` instead. But without nesting all these `mapping` steps, it becomes even simpler. – Holger Jun 26 '17 at 08:28
  • 2
    @Eugene: there was in an earlier version. But it doesn’t doesn’t do the intended thing… – Holger Jun 26 '17 at 08:35