【问题标题】:Java stream collect counting to fieldJava 流收集计数到字段
【发布时间】:2019-09-19 11:31:45
【问题描述】:

是否可以使用Collectors.groupingBy()Collectors.counting() 来计算自定义对象的字段,而不是创建地图并随后对其进行转换。

我有一个用户列表,如下所示:

public class User {
    private String firstName;
    private String lastName;
    // some more attributes

    // getters and setters
}

我想统计所有具有相同名字和姓氏的用户。因此,我有如下自定义对象:

public static class NameGroup {
    private String firstName;
    private String lastName;
    private long count;

    // getters and setters
}

我可以使用这个来收集名称组:

List<NameGroup> names = users.stream()
        .collect(Collectors.groupingBy(p -> Arrays.asList(p.getFirstName(), p.getLastName()), Collectors.counting()))
        .entrySet().stream()
        .map(e -> new NameGroup(e.getKey().get(0), e.getKey().get(1), e.getValue()))
        .collect(Collectors.toList());

使用此解决方案,我必须首先将用户分组到地图,然后将其转换为我的自定义对象。是否可以将所有名称直接计数到nameGroup.count 以避免在列表(或映射)上重复两次并提高性能?

【问题讨论】:

    标签: java java-stream grouping counting


    【解决方案1】:

    我不知道您的要求是什么,但我修改了您的 NameGroup 类以接受 User 类而不是名字和姓氏。然后,这消除了从 List 的中间流和仅从 User 流中选择值的需要。但它仍然需要地图。

          List<NameGroup> names =
                users.stream().collect(Collectors.groupingBy(p -> p,Collectors.counting()))
                              .entrySet().stream()
                              .map(e -> new NameGroup(e.getKey(), e.getValue())).collect(
                                  Collectors.toList());
    

    【讨论】:

      【解决方案2】:
      public static class NameGroup {
          // ...
          @Override
          public boolean equals(Object other) {
              final NameGroup o = (NameGroup) other;
              return firstName.equals(o.firstName) && lastName.equals(o.lastName);
          }
          @Override
          public int hashCode() {
              return Objects.hash(firstName, lastName);
          }
          @Override
          public String toString() {
              return firstName + " " + lastName + " " + count;
          }
      }
      
      public static void main(String[] args) throws IOException {
          List<User> users = new ArrayList<>();
          users.add(new User("fooz", "bar"));
          users.add(new User("fooz", "bar"));
          users.add(new User("foo", "bar"));
          users.add(new User("foo", "bar"));
          users.add(new User("foo", "barz"));
          users.stream()
              .map(u -> new NameGroup(u.getFirstName(), u.getLastName(), 1L))
              .reduce(new HashMap<NameGroup, NameGroup>(), (HashMap<NameGroup, NameGroup> acc, NameGroup e) -> {
                  acc.compute(e, (k, v) -> v == null ? e : new NameGroup(e.firstName, e.lastName, e.count + acc.get(e).count));
                  return acc;
              }, (a, b) -> {
                  b.keySet().forEach(e -> a.compute(e, (k, v) -> v == null ? e : new NameGroup(e.firstName, e.lastName, e.count + a.get(e).count)));
                  return a;
              }).values().forEach(x -> System.out.println(x));
      }
      

      这将输出

      fooz bar 2
      foo barz 1
      foo bar 2
      

      【讨论】:

        【解决方案3】:

        您可以最小化中间对象的分配,例如所有Arrays.asList(...) 对象,通过自己构建地图,而不是使用流式传输。

        这取决于您的NameGroup 是可变的。

        为了让代码更简单,让我们添加两个助手到NameGroup

        public static class NameGroup {
            // fields here
        
            public NameGroup(User user) {
                this.firstName = user.getFirstName();
                this.lastName = user.getLastName();
            }
        
            public void incrementCount() {
                this.count++;
            }
        
            // other constructors, getters and setters here
        }
        

        有了这个,你可以实现如下逻辑:

        Map<User, NameGroup> map = new TreeMap<>(Comparator.comparing(User::getFirstName)
                                                           .thenComparing(User::getLastName));
        users.stream().forEach(user -> map.computeIfAbsent(user, NameGroup::new).incrementCount());
        List<NameGroup> names = new ArrayList<>(map.values());
        

        或者如果你实际上不需要列表,最后一行可以简化为:

        Collection<NameGroup> names = map.values();
        

        【讨论】:

        • 我接受您的解决方案,因为它具有最佳性能。谢谢!
        【解决方案4】:

        不是很干净,但你可以这样做:

        List<NameGroup> convertUsersToNameGroups(List<User> users) {
            return new ArrayList<>(users.stream()
                    .collect(Collectors.toMap(p -> Arrays.asList(p.getFirstName(), p.getLastName()),
                            p -> new NameGroup(p.getFirstName(), p.getLastName(), 1L),
                            (nameGroup1, nameGroup2) -> new NameGroup(nameGroup1.getFirstName(), nameGroup1.getLastName(),
                                    nameGroup1.getCount() + nameGroup2.getCount()))).values());
        }
        

        【讨论】:

          【解决方案5】:

          你可以直接到NameGroup.count领取,但是效率会比你现有的少,不会更高。

          在内部,该映射被用于维护一个数据结构,该结构可以有效地跟踪名称组合并将它们映射到随着找到更多匹配项而更新的计数。重新发明这种数据结构很痛苦,而且不太可能带来有意义的改进。

          您可以尝试直接在地图中收集 NameGroup,而不是通过计数,但大多数方法会再次比您现在拥有的更昂贵,而且肯定更尴尬。

          老实说:您现在拥有的一切都非常好,而且在任何重要的方面都没有效率低下。你几乎肯定应该坚持你所拥有的。

          【讨论】:

          • 另外,当前逻辑的优点是NameGroup可以做到不可变。由于使用流的愿望表明倾向于函数式编程,因此不可变对象是该目标的一部分。
          猜你喜欢
          • 2019-03-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-11-19
          • 2020-07-02
          相关资源
          最近更新 更多