Collecting multiple properties to a Map using a single Stream

Viewed 1025

I have an Userdefined Object which contains multiple properties. As they all have same datatype as String, I wanted to use Streams to collect all those properties values into a Map. I am not aware that Collectors.toMap() or Collectors.groupingBy() is able to serve this.

public class TeamMates{

    private String playerFirstName;
    private String playerLastName;
    private String playerMaidenName;

    //Constructors , getters and Setters    
}

I have an List<TeamMates> object which I wanted to use it to stream and collect it into a Map<String,List<String>>.

I wanted the Map to contain

(playerFirstName,("John","Bob"));
(playerLastName,("Joe","Henry"));
(playerMaidenName,("H","K"));

As I don't wanted to use streaming multiple times to collect each property individually or using forEach, is there any way to get the above properties with a single stream.

2 Answers

First create a map of the value generating getter methods and their respective field names.

// Map.of is available from Java 9. 
// If you are using java 8 you could create the below using a normal HashMap.
Map<String, Function<TeamMates, String>> mapper 
    = Map.of("playerFirstName", TeamMates::getPlayerFirstName,
             "playerLastName", TeamMates::getPlayerLastName,
             "playerMaidenName", TeamMates::getPlayerMaidenName);

Now iterate this mapper function and apply each mapping to the list of teammates

// applies getter method on each teammate object and returns the list of strings.
Function<Function<TeamMates, String>, List<String>> listMapper 
    = fn -> list.stream().map(fn::apply).collect(Collectors.toList());

// assume static import of Collectors.toMap
Map<String, List<String>> map = mapper.entrySet().stream()
                                      .collect(toMap(Entry::getKey,
                                                     e -> listMapper.apply(e.getValue())));

I would also suggest naming your class as TeamMate instead of TeamMates as the class represents a single team mate entity.

Assuming that:

  • An instance of TeamMates holds the information of one person (should it be called TeamMate?)
  • The keys of the output Map are the literal strings "playerFirstName", "playerLastName", "playerMaidenName" (rather than how they appear as variables in the question)
  • The intention is to collect e.g. all of the first names of the objects in the input List into the List corresponding to the "playerFirstName" key in the output Map

Then:

The main complexity with streaming from the initial List to the resulting Map, is that the Map entries don't correspond to the List elements.

Here's one way to do it, using Stream.collect(Supplier supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner) variety of the collect method. See the main method in the below example. Since the arguments are all functions, I've extracted them out to variables to make the stream pipeline easier to read, and also to show the types of each function:

public class TeamMate {

    private static final String PLAYER_MAIDEN_NAME = "playerMaidenName";
    private static final String PLAYER_LAST_NAME = "playerLastName";
    private static final String PLAYER_FIRST_NAME = "playerFirstName";

    /**
     * Passed to Stream::collect, extracted out to show type
     */
    private static final Supplier<Map<String, List<String>>> supplier = () -> Map.of(
            PLAYER_FIRST_NAME, new ArrayList<String>(),
            PLAYER_LAST_NAME, new ArrayList<String>(),
            PLAYER_MAIDEN_NAME, new ArrayList<String>());

    /**
     * Passed to Stream::collect, extracted out to show type
     */
    private static final BiConsumer<Map<String, List<String>>, TeamMate> accumulator = (r, tm) -> {
        r.get(PLAYER_FIRST_NAME).add(tm.getPlayerFirstName());
        r.get(PLAYER_LAST_NAME).add(tm.getPlayerLastName());
        r.get(PLAYER_MAIDEN_NAME).add(tm.getPlayerMaidenName());
    };

    /**
     * Passed to Stream::collect, extracted out to show type
     */
    private static final BiConsumer<Map<String, List<String>>, Map<String, List<String>>> combiner = (x, y) -> {
        x.get(PLAYER_FIRST_NAME).addAll(y.get(PLAYER_FIRST_NAME));
        x.get(PLAYER_LAST_NAME).addAll(y.get(PLAYER_LAST_NAME));
        x.get(PLAYER_MAIDEN_NAME).addAll(y.get(PLAYER_MAIDEN_NAME));
    };

    private String playerFirstName;
    private String playerLastName;
    private String playerMaidenName;

    TeamMate(String playerFirstName, String playerLastName, String playerMaidenName) {
        this.playerFirstName = playerFirstName;
        this.playerLastName = playerLastName;
        this.playerMaidenName = playerMaidenName;
    }

    // getters & setters...

    public static void main(String[] args) {
        
        // input list
        List<TeamMate> teamMates = List.of(new TeamMate("John", "Joe", "H"), new TeamMate("Bob", "Henry", "K"));

        // single stream, as desired
        Map<String, List<String>> output = teamMates.stream().collect(supplier, accumulator, combiner);

        // output map
        System.out.println(output);

        // demonstration that results are as expected
        assert output.containsKey(PLAYER_FIRST_NAME);
        assert output.containsKey(PLAYER_LAST_NAME);
        assert output.containsKey(PLAYER_MAIDEN_NAME);

        assert output.get(PLAYER_FIRST_NAME).containsAll(teamMates.stream().map(TeamMate::getPlayerFirstName).toList());
        assert output.get(PLAYER_LAST_NAME).containsAll(teamMates.stream().map(TeamMate::getPlayerLastName).toList());
        assert output.get(PLAYER_MAIDEN_NAME).containsAll(teamMates.stream().map(TeamMate::getPlayerMaidenName).toList());
    }
}
Related