是否可以将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
以避免在列表(或映射)上重复两次并提高性能?
答案 0 :(得分:2)
您可以直接收集到NameGroup.count
,但是效率会比您现有的低,而不会更高。
在内部,该映射用于维护数据结构,该结构可以有效地跟踪名称组合并将其映射到随着发现更多匹配项而更新的计数。重塑该数据结构是痛苦的,不可能带来有意义的改进。
您可以尝试直接在地图上收集名称组,而不是通过计数来收集名称组,但是大多数方法再一次将比您现在拥有的方法更昂贵,而且当然更尴尬。
老实说:您现在拥有的一切都非常好,并且在任何重要方面都不会低效。您几乎应该肯定会坚持自己拥有的东西。
答案 1 :(得分:2)
不是很干净,但是您可以按照以下方式进行操作:
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());
}
答案 2 :(得分:1)
您可以最小化中间对象的分配,例如所有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();
答案 3 :(得分:0)
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
答案 4 :(得分:0)
我不知道您的要求是什么,但我修改了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());