Suryaprakash Pisay
Suryaprakash Pisay

Reputation: 648

Java 8 Collections max size with count

I have a class like below.

class Student {
    public Student(Set<String> seminars) {
        this.seminar = seminars;
    }
    Set<String> seminar;
    public Set<String> getSeminar()
    {
        return seminar;
    }
}

And created a set of students like below.

List<Student> students = new ArrayList<Student>();
Set<String> seminars = new HashSet<String>();

seminars.add("SeminarA");
seminars.add("SeminarB");
seminars.add("SeminarC");

students.add(new Student(seminars)); //Student 1 - 3 seminars
students.add(new Student(seminars)); //Student 2 - 3 seminars

seminars = new HashSet<String>();
seminars.add("SeminarA");
seminars.add("SeminarB");

students.add(new Student(seminars)); //Student 3 - 2 seminars

seminars = new HashSet<String>();
students.add(new Student(seminars)); //Student 4 - 0 seminars

Now the question is "I'm trying to get the count of students who has atteneded maximus seminars" As you can see there are 2 students who has attended to 3(maximum) seminars, So I needed to get that count.

I achieved the same using 2 different statements using stream

OptionalInt max = students.stream()
    .map(Student::getSeminar)
    .mapToInt(Set::size)
    .max();
long count = students.stream()
    .map(Student::getSeminar)
    .filter(size -> size.size() == max.getAsInt())
    .count();

is there a way to achieve the same using one statement?

Upvotes: 4

Views: 7376

Answers (4)

Alex Mamo
Alex Mamo

Reputation: 139039

To solve this, please use the follwing code:

students.stream()
    .map(Student::getSeminar)
    .collect(Collectors.groupingBy(Set::size, Collectors.counting()));

Your output will be:

{0=1, 2=1, 3=2}

As you can see, there are two students that are going to the maximum number of seminars which is three.

First we changed all the students objects from the stream with the actual sets and then we used Collectors groupingBy() method to group the sets by size.

If you want to get only the number of the students, please use following code:

students.stream()
    .map(Student::getSeminar)
    .collect(Collectors.groupingBy(Set::size, Collectors.counting()))
    .values().stream()
    .max(Comparator.comparing(a->a))
    .get();

Your output will be: 2.

Upvotes: 8

Misha
Misha

Reputation: 28183

In most practical situations, your approach of traversing the list twice is the best one. It is simple and the code is easy to understand. Fight the urge to conserve statements -- you don't get charged per semicolon!

However, there are some conceivable scenarios when the source data cannot be traversed twice. Perhaps it comes from some slow or once-only source like a network stream.

If you have such a situation, you might want to define a custom collector allMaxBy that collects all max elements into a downstream collector.

Then you would be able to write

long maxCount = students.stream()
    .collect(allMaxBy(
            comparingInt(s -> s.getSeminar().size()),
            counting()
    ));

Here's an implementation for allMaxBy:

public static <T, A, R> Collector<T, ?, R> allMaxBy(Comparator<? super T> cmp, Collector<? super T, A, R> downstream) {
    class AllMax {
        T val;
        A acc = null; // null means empty

        void add(T t) {
            int c = acc == null ? 1 : cmp.compare(t, val);
            if (c > 0) {
                val = t;
                acc = downstream.supplier().get();
                downstream.accumulator().accept(acc, t);
            } else if (c == 0) {
                downstream.accumulator().accept(acc, t);
            }
        }

        AllMax merge(AllMax other) {
            if (other.acc == null) {
                return this;
            } else if (this.acc == null) {
                return other;
            }
            int c = cmp.compare(this.val, other.val);
            if (c == 0) {
                this.acc = downstream.combiner().apply(this.acc, other.acc);
            }
            return c >= 0 ? this : other;
        }

        R finish() {
            return downstream.finisher().apply(acc);
        }
    }

    return Collector.of(AllMax::new, AllMax::add, AllMax::merge, AllMax::finish);
}

Upvotes: 1

Saravana
Saravana

Reputation: 12817

Little modified @Alex's solution, to sort the keys while doing the grouping

    TreeMap<Integer, Long> agg = students.stream()
            .map(Student::getSeminar)
            .collect(Collectors.groupingBy(Set::size, TreeMap::new, Collectors.counting()));
    System.out.println(agg);
    System.out.println(agg.lastEntry().getKey() + " - " + agg.lastEntry().getValue());

output

3 - 2

Upvotes: 0

randomnetcat
randomnetcat

Reputation: 60

This is ugly, but the following would work:

long count = students.stream()
    .filter(s -> s.getSeminar().size() == students
        .stream().mapToInt(a -> a.getSeminar().size())
        .max().orElse(0))
    .count();

Explanation: This streams the students list and filters the students so that only students with the maximum number of seminars (which is what the nested lambda is) remain and then takes the count of that stream.

Upvotes: 0

Related Questions