【问题标题】:How to use Java 8 Streams groupingBy to map multiple database records to a single object with a List<String> property for one column如何使用 Java 8 Streams groupingBy 将多个数据库记录映射到具有 List<String> 属性的单个对象的一列
【发布时间】:2016-06-05 05:19:36
【问题描述】:

我的问题基本上归结为这个简化的例子。我有从数据库返回的数据,其中行中有一些重复的信息。

在此示例中,我有一个从数据库返回的 TeamRow 对象列表。我可以使用Collectors.groupingBy 轻松地将这些分组:

public class TeamRow {
    private int id;
    private String name;
    private String player;

    public TeamRow(int id, String name, String player) {
        this.id = id;
        this.name = name;
        this.player = player;
    }

    public int getId() {return id;}
    public String getName() { return name; }
    public String getPlayer() {return player;}
}

public class Team {
    private int id;
    private String name;
    private List<String> players;

    public Team(int id, String name, List<String> players) {
        this.id = id;
        this.name = name;
        this.players = new ArrayList<String>(players);
    }
}

List<TeamRow> dbTeams = new ArrayList<TeamRow>();
dbTeams.add(new TeamRow(1, "Team1", "Jonny"));
dbTeams.add(new TeamRow(1, "Team1", "Rob"));
dbTeams.add(new TeamRow(1, "Team1", "Carlos"));
dbTeams.add(new TeamRow(2, "Team2", "Shane"));
dbTeams.add(new TeamRow(2, "Team2", "Lucas"));
dbTeams.add(new TeamRow(3, "Team3", "Geraint"));
dbTeams.add(new TeamRow(3, "Team3", "Rocky"));
dbTeams.add(new TeamRow(3, "Team3", "Wayne"));
dbTeams.add(new TeamRow(3, "Team3", "Dwayne"));
dbTeams.add(new TeamRow(3, "Team3", "Lester"));

Map<Integer, List<TeamRow>> myMap = dbTeams.stream().collect(Collectors.groupingBy(TeamRow::getId));

但是,我实际上想要实现的是将TeamRows 转换为Teams。这样 id 和 name 只表示一次,并且玩家存储在 Team 对象的 List 中。我可以通过在地图上添加forEach 来实现这一点,如下所示。

但我一直试图弄清楚是否有一种方法可以通过添加某种映射器或下游收集器来实现相同的结果。这甚至比添加后续的forEach 有什么好处吗?例如:

List<Team> teams = dbTeams.stream().collect(Collectors.groupingBy(TeamRow::getId, ???), ???).???; 

使用 forEach 进行转换:

List<Team> teams = new ArrayList<>();
myMap.forEach((id, teamRows) -> {
    if (teamRows.size() > 0) {
        TeamRow tr = teamRows.get(0);
        List<String> players = teamRows.stream().map(TeamRow::getPlayer).collect(Collectors.toList());
        teams.add(new Team(id, tr.getName(), players));
    }
});

【问题讨论】:

    标签: java mapping grouping java-stream collectors


    【解决方案1】:

    你可以试试

        List<Team> teamList = dbTeams.stream().collect(Collectors.collectingAndThen(Collectors.groupingBy(TeamRow::getId),
                (m -> m.entrySet().stream().map(
                e -> {
                    List<TeamRow> l = e.getValue();
                    return new Team(l.get(0).getId(), l.get(0).getName(), l.stream().map(TeamRow::getPlayer).collect(Collectors.toList()));
    
                }
        ).collect(Collectors.toList()))));
    

    使用collectingAndThen(),您可以使用将映射条目映射到Teams 的函数。 l.get(0) 不应失败,因为列表中始终至少有一个条目。 我不确定这是否更简洁,但至少它不使用foreach。

    【讨论】:

    • 谢谢。我一直在寻找collectiveAndThen,但在使用所需的语法时有点挣扎。请问这与在groupingBy期间生成的Map上单独调用forEach之间是否有任何重大区别?一种方法比另一种更有效吗??
    • @procke:我认为forEach 和使用collectingAndThen 在性能方面没有太大区别。但是使用forEach 的解决方案使用了一个可变的ArrayList,如果map.forEach 并行运行可能会出现问题(尽管默认实现可能不会这样做)。但除此之外,由于整个流 API 都是为了避免可变性,我更喜欢没有可变列表的解决方案。
    【解决方案2】:

    之前我说过我会通过创建这样的原子转换器函数来做到这一点:

    Function<TeamRow, Team> getTeamRowTransformer() {
        final Map<Integer, Team> map = new ConcurrentHashMap<Integer, Team>();
        return (teamRow)->{
            Team result = map.computeIfAbsent(teamRow.getId(), id->new Team(id, teamRow.getName(), Collections.emptyList()));
            result.players.add(teamRow.getPlayer());
            return result;
        };
    }
    

    它处理映射,您的流代码成为一个非常清晰的步骤:

    Set<Team> finalTeams = dbTeams.stream()
        .map(getTeamRowTransformer())
        .collect(Collectors.toSet());
    

    但是,我意识到,您也可以这样做:

        List<Team> teams = dbTeams.stream()
            .map(tr->new Team(tr.getId(), tr.getName(), Arrays.asList(tr.getPlayer())))
            .collect(Collectors.collectingAndThen(
                        Collectors.groupingBy(t->t.id, 
                            Collectors.reducing((Team a, Team b)->{
                                a.players.addAll(b.players); 
                                return (Team)a;
                            })
                        ), m->m.values().stream()
                        .filter(Optional::isPresent)
                        .map(Optional::get)
                        .collect(Collectors.toList())
                    )
            );
    

    这样,在分配 List&lt;Team&gt; teams 之前,您永远不会拥有可访问的可变集合。

    【讨论】:

      【解决方案3】:

      您可以使用带有自定义合并功能的toMap 收集器。将merge 方法添加到Team 类可能是个好主意:

      public class Team {
          private final int id;
          private final String name;
          private final List<String> players;
      
          public Team(int id, String name, List<String> players) {
              this.id = id;
              this.name = name;
              this.players = new ArrayList<>(players);
          }
      
          // merges other team into this team, returning this team
          public Team merge(Team other) {
              assert id == other.id; // remove asserts if you don't like them
              assert name.equals(other.name);
              players.addAll(other.players);
              return this;
          }
      }
      

      现在你可以这样解决你的问题:

      Collection<Team> teams = dbTeams.stream()
              .map(tr -> new Team(tr.id, tr.name, Arrays.asList(tr.player)))
              .collect(Collectors.toMap(t -> t.id, t -> t, Team::merge)).values();
      

      【讨论】:

        猜你喜欢
        • 2015-04-15
        • 1970-01-01
        • 2019-07-01
        • 2021-10-12
        • 2017-06-01
        • 1970-01-01
        • 1970-01-01
        • 2020-06-27
        • 2015-08-13
        相关资源
        最近更新 更多