使用单个 Stream 将多个属性收集到 Map

Collecting multiple properties to a Map using a single Stream

我有一个包含多个属性的用户定义对象。由于它们都具有与 String 相同的数据类型,我想使用 Streams 将所有这些属性值收集到 Map 中。我不知道 Collectors.toMap()Collectors.groupingBy() 可以提供此服务。

public class TeamMates{

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

    //Constructors , getters and Setters    
}

我有一个 List<TeamMates> 对象,我想用它来流式传输并将其收集到 Map<String,List<String>>

我希望地图包含

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

因为我不想多次使用流式处理来单独收集每个 属性 或使用 forEach,有没有办法通过单个流获得上述属性。

假设:

  • TeamMates的一个实例保存了一个人的信息(应该叫TeamMate吗?)
  • 输出 Map 的键是文字字符串 "playerFirstName""playerLastName""playerMaidenName"(而不是它们在问题中作为变量的显示方式)
  • 目的是收集例如输入 List 中对象的所有名字到 List 对应于输出 Map
  • 中的 "playerFirstName"

然后:

从初始 List 流式传输到结果 Map 的主要复杂性在于 Map 条目与 List 元素不对应。

这是一种方法,使用 Stream.collect(Supplier supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner) 多种收集方法。请参阅下面示例中的 main 方法。由于参数都是函数,我将它们提取到变量中以使流管道更易于阅读,并显示每个函数的类型:

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());
    }
}

首先创建值生成 getter 方法及其各自字段名称的映射。

// 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);

现在迭代此映射器函数并将每个映射应用于队友列表

// 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())));

我还建议将您的 class 命名为 TeamMate 而不是 TeamMates,因为 class 代表单个队友实体。