Java stream – find most frequent element based on a specific field

I have a list of Person objects, I would like to find the most frequent name in the list, and the frequency, only using java streams. (When there is a tie, return any result)

Currently, my solution uses groupingBy and counting, then again finding the max element in the resulting map. The current solution makes 2 passes on the input (list/map). Is it possible to make this a bit more efficient and readable?

Person p1 = Person.builder().id("p1").name("Alice").age(1).build();
Person p2 = Person.builder().id("p2").name("Bob").age(2).build();
Person p3 = Person.builder().id("p3").name("Charlie").age(3).build();
Person p4 = Person.builder().id("p4").name("Alice").age(4).build();
List<Person> people = ImmutableList.of(p1, p2, p3, p4);

Map.Entry<String, Long> mostCommonName = people
        .stream()
        .collect(collectingAndThen(groupingBy(Person::getName, counting()),
                map -> map.entrySet().stream().max(Map.Entry.comparingByValue()).orElse(null)
        ));

System.out.println(mostCommonName); // Alice=2

Answer

If you are insisting on only using streams then your best bet is likely to have a custom collector that includes the info required to aggregate in a single pass:

class MaxNameFinder implements Collector<Person, ?, String> {
    public class Accumulator {
        private final Map<String,Integer> nameFrequency = new HashMap<>();
        private int modeFrequency = 0;
        private String modeName = null;

        public String getModeName() {
            return modeName;
        }

        public void accept(Person person) {
            currentFrequency = frequency.merge(p.getName(), 1, Integer::sum);
            if (currentFrequency > modeFrequency) {
                modeName = person.getName();
                modeFrequency = currentFrequency;
            }
        }

        public Accumulator combine(Accumulator other) {
            other.frequency.forEach((n, f) -> this.frequency.merge(n, f, Integer::sum));
            if (this.frequency.get(other.modeName) > frequency.get(this.modeName))
                modeName = other.modeName;
            modeFrequency = frequency.get(modeName);
            return this;
        };

    }

    public BiConsumer<Accumulator,​Person> accumulator() {
        return Accumulator::accept;
    }

    public Set<Collector.Characteristics> characteristics() {
        return Set.of(Collector.Characteristics.CONCURRENT);
    }

    public BinaryOperator<Accumulator> combiner() {
        return Accumulator::combine;
    }

    public Function<Accumulator,String> finisher() {
        return Accumulator::getModeName;
    }

    public Supplier<Accumulator> supplier() {
        return Accumulator::new;
    }
}

Usage would be:

people.stream().collect(new MaxNameFinder())

which would return a string representing the most common name.