将两个自定义类型的列表相加

5

我有一个简单的User类,它有一个String和一个int属性。

我想以这种方式添加两个用户列表:

  • 如果字符串相等,则应将数字相加,并成为其新值。
  • 新列表应包括所有具有适当值的用户。

像这样:

List1: { [a:2], [b:3] }
List2: { [b:4], [c:5] }
ResultList: {[a:2], [b:7], [c:5]}

User 定义:

public class User { 
    private String name;
    private int comments;
}

我的方法:

public List<User> addTwoList(List<User> first, List<User> sec) {
    List<User> result = new ArrayList<>();
    for (int i=0; i<first.size(); i++) {
        Boolean bsin = false;
        Boolean isin = false;
        for (int j=0; j<sec.size(); j++) {
            isin = false; 
            if (first.get(i).getName().equals(sec.get(j).getName())) {
                int value= first.get(i).getComments() + sec.get(j).getComments();
                result.add(new User(first.get(i).getName(), value));
                isin = true;
                bsin = true;
            }
            if (!isin) {result.add(sec.get(j));}
        }
        if (!bsin) {result.add(first.get(i));}
    }
    return result;      
}

但它将许多东西添加到了列表中。

4个回答

5

最好使用toMap收集器来完成此操作:

 Collection<User> result = Stream
    .concat(first.stream(), second.stream())
    .collect(Collectors.toMap(
        User::getName,
        u -> new User(u.getName(), u.getComments()),
        (l, r) -> {
            l.setComments(l.getComments() + r.getComments());
            return l;
        }))
    .values();
  • 首先,通过Stream.concat将两个列表合并成单个Stream<User>
  • 其次,我们使用toMap收集器来合并具有相同Name的用户,并返回Collection<User>结果。

如果您严格地想要一个List<User>,那么请将结果传递给ArrayList构造函数,即List<User> resultSet = new ArrayList<>(result);


感谢 @davidxxx,您可以直接从管道收集到列表中,并避免使用中间变量创建:

List<User> result = Stream
    .concat(first.stream(), second.stream())
    .collect(Collectors.toMap(
         User::getName,
         u -> new User(u.getName(), u.getComments()),
         (l, r) -> {
              l.setComments(l.getComments() + r.getComments());
              return l;
         }))
    .values()
    .stream()
    .collect(Collectors.toList());

1
或者为什么不使用.values().stream().collect(toList())来避免定义额外的变量。 - davidxxx
1
@davidxxx 如果不想创建一个变量,那么我建议直接将大部分管道传递给构造函数,这会导致一定程度上的可读性降低,所以你的建议确实很好。我会进行编辑以适应。 - Ousmane D.
1
@davidxxx 确实,那是另一个选项,但我会避免使用它,因为你已经暗示它不够内存友好。更好的选择是将合并函数提取到 User 内部的一个方法中,然后使用管道中的方法引用调用它,这将导致更好的可读性并避免开销。例如,该方法将如下所示:public User merge(User other) { setComments(getComments() + other.getComments()); return this; } 然后在上面的合并函数的位置使用 User::merge - Ousmane D.
1
@Aomine:谢谢,不用谢。[tag:java-stream] 最大的问题在于可读性和弄清楚收集器确切需要什么。 - Nikolas Charalambidis
1
如果内存使用确实是一个问题,那么我会考虑使用.collect(groupingBy(User::getName, summingInt(User::getComments))),然后将每个条目映射到一个User对象。;-) - Ousmane D.
显示剩余5条评论

3

您需要使用一个中间映射来通过将用户的年龄相加来合并两个列表中的用户。

一种方法是使用流,就像Aomine的答案中所示。这里有另一种方法,不使用流:

Map<String, Integer> map = new LinkedHashMap<>();
list1.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
list2.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));

现在,您可以按照以下方式创建用户列表:
List<User> result = new ArrayList<>();
map.forEach((name, comments) -> result.add(new User(name, comments)));

这里假设User有一个接受namecomments参数的构造函数。

编辑:如@davidxxx所建议,我们可以通过提取第一部分来改进代码:

BiConsumer<List<User>, Map<String, Integer>> action = (list, map) -> 
        list.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));

Map<String, Integer> map = new LinkedHashMap<>();
action.accept(list1, map);
action.accept(list2, map);

这次重构将避免DRY。

1
@Nikolas,Federico 的意思是,使用 Stream.concat(list1.stream(), list2.stream()).forEach(u->...),就像 David 建议的那样,会涉及不必要的副作用,因为你随后在外部累加到一个 map 中。流和副作用不应该同时出现,尤其是在并行运行时更是如此。如果我理解有误,请 Federico 纠正我;-)但我认为这就是你的意思。 - Ousmane D.
1
@Aomine:我在谈论(list, map) -> list.forEachstream根本没有被构建,而forEach方法会引起副作用——这是可以接受的。为什么Stream::forEach不被鼓励使用,因为它与Iterable::forEach相同?我完全理解并行流的副作用问题,我不建议使用例如stream-map-filter-forEach,但是Stream::forEach可能是可以接受的吗? - Nikolas Charalambidis
@Aomine 我的意思就是那个 :) - fps
1
@Nikolas 是的,我认为你是正确的。在这种特定情况下不会有任何问题。然而,我宁愿严格遵循这个规则,永远不使用那个模式。如果您喜欢,这只是为了风格原因...此外,即使它永远不会发生,您也不知道 Stream.concat 是否会使流并行(它不应该,但是)... - fps
这就是我的意思,谢谢。Stream.concat的行为对我来说是新的,如果其中一个输入是并行的,它会变成并行的(来自文档)。这是否意味着在这种情况下使用Stream.of(...).flatMap(List::stream)...更安全? - Nikolas Charalambidis
显示剩余5条评论

2

有一种相当直接的方法,使用 Collectors.groupingByCollectors.reducing,它不需要 setter,这是最大的优点,因为你可以保持 User 的不变性:

Collection<Optional<User>> d = Stream
    .of(first, second)                    // start with Stream<List<User>> 
    .flatMap(List::stream)                // flatting to the Stream<User>
    .collect(Collectors.groupingBy(       // Collecting to Map<String, List<User>>
        User::getName,                    // by name (the key)
                                          // and reducing the list into a single User

        Collectors.reducing((l, r) -> new User(l.getName(), l.getComments() + r.getComments()))))
    .values();                            // return values from Map<String, List<User>>

很遗憾,结果是Collection<Optional<User>>,因为减少管道返回Optional,由于结果可能不存在。您可以流式传输值并使用map()来去除Optional,或者使用Collectors.collectAndThen*:
Collection<User> d = Stream
    .of(first, second)                    // start with Stream<List<User>> 
    .flatMap(List::stream)                // flatting to the Stream<User>
    .collect(Collectors.groupingBy(       // Collecting to Map<String, List<User>>       
        User::getName,                    // by name (the key)
        Collectors.collectingAndThen(     // reduce the list into a single User

            Collectors.reducing((l, r) -> new User(l.getName(), l.getComments() + r.getComments())), 
            Optional::get)))              // and extract from the Optional
    .values();  

* Thanks to @Aomine


1
你可以将reducing收集器传递给collectingAndThen收集器,并使用Optional::get作为完成函数,以使事情更简短、更容易。例如:.collect(Collectors.groupingBy( User::getName, collectingAndThen( Collectors.reducing((l, r) -> new User(l.getName(), l.getComments() + r.getComments())), Optional::get)))。这意味着您不需要后续的.values().stream()..... - Ousmane D.
1
太好了!最棒的是回答问题让我学到更多。谢谢 :) - Nikolas Charalambidis

2
作为一种替代方法,可以采用以下简单而高效的方式:
  • 将元素流式传输
  • 将它们收集到一个Map<String, Integer>中,以将每个名称与评论总数(int)关联起来
  • 将收集到的映射表的条目流式传输,以创建用户列表。
另外,在第三步中,您可以使用collectingAndThen(groupingBy()..., m -> ...Map收集器进行最后的转换,但我不认为它总是非常易读,而且在这里我们可以不用它。
结果如下:
List<User> users =
        Stream.concat(first.stream(), second.stream())
              .collect(groupingBy(User::getName, summingInt(User::getComments)))
              .entrySet()
              .stream()
              .map(e -> new User(e.getKey(), e.getValue()))
              .collect(toList());

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接