The problem

I have a list of Person objects. I would like to group them per birth month, and only keep a List of lastnames.

I came up with this code from memory

Map<String, List<Person>> personsByMonth = persons.stream()
        .collect(Collectors.groupingBy(Person::getBirthMonth));
System.out.println(personsByMonth);

And you get the following output (truncated for readability):

{JUNE=[Person{lastName='Doe 11', birthDate=2022-06-07}], JANUARY=[Person{lastName='Doe 10', birthDate=2023-01-26}]}

But how do I get rid of the redundant information?

The solution

The Collectors class provides quite a lot of variation of the groupingBy method. The one I need is the two parameter version. It also has a snippet that clarifies a lot. The final code now is:

Map<String, List<String>> namesByMonth = persons.stream()
      .collect(Collectors.groupingBy(Person::getBirthMonth,  // group by birthMonth
              Collectors.mapping(Person::getLastName, Collectors.toList()))); // map each person to their lastName and collect them in a list

Sample run

Let’s put it all together and do a test run:

package com.emakina.vb.poc.rat;

import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Person {
    private String lastName;
    private LocalDate birthDate;

    public Person(String lastName, LocalDate birthDate) {
        this.lastName = lastName;
        this.birthDate = birthDate;
    }

    public String getLastName() {
        return lastName;
    }

    public LocalDate getBirthDate() {
        return birthDate;
    }

    public String getBirthMonth(){
        return getBirthDate().getMonth().name();
    }

    @Override
    public String toString() {
        return "Person{" +
                "lastName='" + lastName + '\'' +
                ", birthDate=" + birthDate +
                '}';
    }

    // helper method to generate a random date up till a year ago
    public static LocalDate randomDate(){
        return LocalDate.now().minusDays(new Random().nextInt(366));
    }

    public static void main(String[] args){
        //generate John Doe's
        List<Person> persons = IntStream.range(0,20)
                .mapToObj(index->new Person("Doe "+ index, randomDate()))
                .collect(Collectors.toList());

        Map<String, List<Person>> personsByMonth = persons.stream()
                .collect(Collectors.groupingBy(Person::getBirthMonth));
        System.out.println(personsByMonth);

        Map<String, List<String>> namesByMonth = persons.stream()
                .collect(Collectors.groupingBy(Person::getBirthMonth,
                        Collectors.mapping(Person::getLastName, Collectors.toList())));

        System.out.println(namesByMonth);


    }
}

Sample output (truncated for readability):

{JUNE=[Person{lastName='Doe 11', birthDate=2022-06-07}], JANUARY=[Person{lastName='Doe 10', birthDate=2023-01-26}]}
{JUNE=[Doe 11], JANUARY=[Doe 10], MAY=[Doe 3, Doe 5, Doe 6, Doe 12], MARCH=[Doe 7, Doe 16], FEBRUARY=[Doe 9], AUGUST=[Doe 4, Doe 15], SEPTEMBER=[Doe 17, Doe 19], JULY=[Doe 1, Doe 18], NOVEMBER=[Doe 2, Doe 14], APRIL=[Doe 0, Doe 8, Doe 13]}