【问题标题】:Adding two lists of own type添加两个自己类型的列表
【发布时间】:2019-05-29 08:39:32
【问题描述】:

我有一个简单的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;      
}

但它在列表中添加了很多东西。

【问题讨论】:

    标签: java list for-loop arraylist collections


    【解决方案1】:

    最好通过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&lt;User&gt;
    • 其次,我们使用toMap 收集器来合并恰好具有相同Name 的用户并返回Collection&lt;User&gt; 的结果。

    如果您严格想要List&lt;User&gt;,则将结果传递给ArrayList构造函数,即List&lt;User&gt; resultSet = new ArrayList&lt;&gt;(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());
    

    【讨论】:

    • 或者为什么不.values().stream().collect(toList()) 以避免定义额外的变量。
    • @davidxxx 如果不​​想创建变量,那么我建议将大部分管道直接传递给构造函数,这会导致可读性降低,所以是的,您的建议是确实是好点。我会编辑以适应。
    • @davidxxx 确实,这是另一种选择,但我会远离它,因为您已经暗示它对内存不友好。更好的选择是将合并函数提取到User 中的方法,然后使用管道中的方法引用调用它,这将提高可读性并避免开销。例如,该方法看起来像 public User merge(User other) { setComments(getComments() + other.getComments()); return this; } 然后使用 User::merge 代替上面的合并函数。
    • @Aomine:谢谢,不客气。 java-stream 的最大问题是可读性和弄清楚收集器到底需要什么的能力。
    • @davidxxx 在这种情况下,如果内存使用情况确实值得关注,那么我会考虑使用.collect(groupingBy(User::getName, summingInt(User::getComments))),然后将每个条目映射到User 对象。 ;-)
    【解决方案2】:

    您必须使用中间映射来合并两个列表中的用户,方法是对他们的年龄求和。

    一种方法是使用流,如Aomine's answer 所示。这是另一种方式,没有流:

    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。

    【讨论】:

    • 很好 :) 你可以排除第一部分:Stream.concat(list1.stream(), list2.stream()).forEach(u-&gt;...)
    • @davidxxx 我的想法完全一样 ;) 但是从 withing Stream.forEach 触摸地图不是流友好的(在文档中不鼓励这样做)...
    • @Nikolas Federico 的意思是使用 david 建议的 Stream.concat(list1.stream(), list2.stream()).forEach(u-&gt;...) 会带来不必要的副作用,因为您随后会在外部累积地图。流和副作用不应该同时出现。并行运行时更是如此。如果我错了,Federico 纠正我 ;-) 但我认为这就是你的意思。
    • @Aomine:我说的是(list, map) -&gt; list.forEachstream 根本没有构造,forEach 方法会导致副作用 - 这很好。为什么不鼓励使用Stream::forEach,因为它与Iterable::forEach 相同?我完全理解并行流的副作用问题,我不建议使用 ex。 stream-map-filter-forEach,但是 Stream::forEach 可能没问题?。
    • @Nikolas 是的,我认为你是正确的。在这种特定情况下不会有任何问题。但是,我宁愿严格遵守这一点,永远不要使用那种模式。这只是出于风格的原因,如果您愿意的话...此外,即使它永远不会发生,您也不知道Stream.concat 是否使流并行(它不应该, 但是)...
    【解决方案3】:

    使用Collectors.groupingByCollectors.reducing 有一种非常直接的方法,不需要需要设置器,这是最大的优势,因为您可以保持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&lt;Optional&lt;User&gt;&gt;,因为归约管道返回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();  
    

    * 感谢@Aomine

    【讨论】:

    • 您可以将reducing 收集器传递给collectingAndThen 收集器,并使用Optional::get 作为整理函数,以使事情变得更短更容易,例如.collect(Collectors.groupingBy( User::getName, collectingAndThen( Collectors.reducing((l, r) -&gt; new User(l.getName(), l.getComments() + r.getComments())), Optional::get)))。这意味着您不需要后续的.values().stream().....
    • 太棒了!回答问题的最佳方式是我学到了更多。谢谢你:)
    【解决方案4】:

    作为替代相当直接和高效:

    • 流式传输元素
    • 将它们收集到Map&lt;String, Integer&gt; 中,以将每个名称与 cmets 的总和相关联 (int)
    • 流式传输收集到的地图条目以创建用户列表。

    或者,对于第三步,您可以使用 collectingAndThen(groupingBy()..., m -&gt; ...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());
    

    【讨论】:

      猜你喜欢
      • 2011-08-20
      • 2012-05-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-02-03
      • 2011-08-22
      • 1970-01-01
      • 2019-01-10
      相关资源
      最近更新 更多