Ger
Ger

Reputation: 649

java 8 grouping by with distinct count

SELECT Count(1) AS total,
          'hello' AS filter,
          field1 AS field1,
          Count(DISTINCT field2) AS total_field2
   FROM table
   WHERE field = true
     AND status = 'ok'
      GROUP  BY field1

Doubts how to make a map using java8 to store the following result. Map key must be field field1 and map value must be total_field2 field.

That is, I need to group my list using field field1 and count field field2

For the total field I have

myList.stream().collect(Collectors.groupingBy(MyObject::getField1, Collectors.counting())) 
// this is just counting the records grouped by field1

My result is correct total_field1: {4=55, 6=31}

For field2, I needed something like this, but it's just giving me a record

myList.stream().filter(distinctByKey(MyObject::getField2))
.collect(Collectors.groupingBy(MyObject::getField1, Collectors.counting()));

public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
        Set<Object> seen = ConcurrentHashMap.newKeySet();
        return t -> seen.add(keyExtractor.apply(t));
    }

Result total_Field2: {4=31}

should return me 2 example records total_Field2: {4=31, 6=31}

Example @Naman

public static <T, A, R> Collector<T, ?, R> filtering(
        Predicate<? super T> predicate, Collector<? super T, A, R> downstream) {

        BiConsumer<A, ? super T> accumulator = downstream.accumulator();
        return Collector.of(downstream.supplier(),
            (r, t) -> { if(predicate.test(t)) accumulator.accept(r, t); },
            downstream.combiner(), downstream.finisher(),
            downstream.characteristics().toArray(new Collector.Characteristics[0]));
    }

myList.stream().collect(Collectors.groupingBy(MyObject::getField1, filtering(distinctByKey(MyObject::getField2), Collectors.counting())));

Upvotes: 7

Views: 10748

Answers (3)

user_3380739
user_3380739

Reputation: 1254

You can try this:

myList.stream().map(obj -> Pair.of(obj.getField1(), obj.getField2()))
      .distinct()
      .collect(Collectors.groupingBy(Pair::getLeft, counting()));

Upvotes: 0

Naman
Naman

Reputation: 31908

An alternate to Deadpool's answer is to count distinctByKey after groupingBy field1 while mapping to entries and then finally collecting to a Map as:

Map<String, Long> r = myList.stream()
        .collect(Collectors.groupingBy(MyObject::getField1))
        .entrySet().stream()
        .map(e -> new AbstractMap.SimpleEntry<>(e.getKey(),
                e.getValue().stream().filter(distinctByKey(MyObject::getField2)).count()))
        .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));

If you were on Java-9 or above, you could have used Collectors.filtering as a downstream with the Predicate defined using the utility distinctByKey such as :

Map<String, Long> result = myList.stream()
        .collect(Collectors.groupingBy(MyObject::getField1,
                Collectors.filtering(distinctByKey(MyObject::getField2),
                        Collectors.counting())));


Note: The above two approaches are quite different though, the former groups all the list items by one field (field1) and then within each subgroup finds a distinct count by another specific field(field2).

On the other hand, the latter groups all the distinct items by the key(field2) and then groups these by another key(field1) with counting reduction.

Upvotes: 2

Ryuzaki L
Ryuzaki L

Reputation: 40068

Actually i used Set to eliminate the duplicates and Collectors.collectingAndThen to get size

Map<String, Integer> res =  list.stream()
                                .collect(Collectors.groupingBy(MyObject::getField1, 
                                        Collectors.mapping(MyObject::getField2, 
                                            Collectors.collectingAndThen(Collectors.toSet(), set->set.size()))));

As per suggestion by @Naman you can also use method reference Set::size

Collectors.collectingAndThen(Collectors.toSet(), Set::size))));

Upvotes: 7

Related Questions