Zemer
Zemer

Reputation: 371

java 8 - stream, map and count distinct

My first attempt with java 8 streams...

I have an object Bid, which represents a bid of a user for an item in an auction. i have a list of bids, and i want to make a map that counts in how many (distinct) auctions the user made a bid.

this is my take on it:

bids.stream()
         .collect(
             Collectors.groupingBy(
                  bid ->  Bid::getBidderUserId, 
                  mapping(Bid::getAuctionId, Collectors.toSet())
             )
         ).entrySet().stream().collect(Collectors.toMap(
             e-> e.getKey(),e -> e.getValue().size())
        );

It works, but i feel like i'm cheating, cause i stream the entry sets of the map, instead of doing a manipulation on the initial stream... must be a more correct way of doing this, but i couldn't figure it out...

Thanks

Upvotes: 19

Views: 20751

Answers (2)

Ruben
Ruben

Reputation: 4056

Tagir Valeev's answer is the right one (+1). Here is an additional one that does exactly the same using your own downstream Collector for the groupBy:

    Map<Integer, Long> map = bids.stream().collect(
               Collectors.groupingBy(Bid::getBidderUserId, 
                                     new Collector<Bid, Set<Integer>, Long>() {

        @Override
        public Supplier<Set<Integer>> supplier() {
            return HashSet::new;
        }

        @Override
        public BiConsumer<Set<Integer>, Bid> accumulator() {
            return (s, b) -> s.add(b.getAuctionId());
        }

        @Override
        public BinaryOperator<Set<Integer>> combiner() {
            return (s1, s2) -> {
                s1.addAll(s2);
                return s1;
            };
        }

        @Override
        public Function<Set<Integer>, Long> finisher() {
            return (s) -> Long.valueOf(s.size());
        }

        @Override
        public Set<java.util.stream.Collector.Characteristics> characteristics() {
            return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.UNORDERED, Collector.Characteristics.IDENTITY_FINISH));
        }
    }));

Upvotes: 0

Tagir Valeev
Tagir Valeev

Reputation: 100169

You can perform groupingBy twice:

Map<Integer, Map<Integer, Long>> map = bids.stream().collect(
        groupingBy(Bid::getBidderUserId,
                groupingBy(Bid::getAuctionId, counting())));

This way you have how many bids each user has in each auction. So the size of internal map is the number of auctions the user participated. If you don't need the additional information, you can do this:

Map<Integer, Integer> map = bids.stream().collect(
        groupingBy(
                Bid::getBidderUserId,
                collectingAndThen(
                        groupingBy(Bid::getAuctionId, counting()),
                        Map::size)));

This is exactly what you need: mapping of users to number of auctions user participated.

Update: there's also similar solution which is closer to your example:

Map<Integer, Integer> map = bids.stream().collect(
        groupingBy(
                Bid::getBidderUserId,
                collectingAndThen(
                        mapping(Bid::getAuctionId, toSet()),
                        Set::size)));

Upvotes: 27

Related Questions