Java 分组并减少对象列表
我有一个对象列表,其中有许多重复的对象和一些需要合并的字段。我想将其简化为仅使用Java8流的唯一对象列表(我知道如何通过旧的skool方法实现这一点,但这只是一个实验) 这就是我现在拥有的。我真的不喜欢这样,因为映射构建似乎是无关的,values()集合是支持映射的视图,您需要将其包装在新的Java 分组并减少对象列表,java,java-8,Java,Java 8,我有一个对象列表,其中有许多重复的对象和一些需要合并的字段。我想将其简化为仅使用Java8流的唯一对象列表(我知道如何通过旧的skool方法实现这一点,但这只是一个实验) 这就是我现在拥有的。我真的不喜欢这样,因为映射构建似乎是无关的,values()集合是支持映射的视图,您需要将其包装在新的ArrayList(…)中以获得更具体的集合。是否有更好的方法,可能使用更通用的缩减操作 @Test public void reduce() { Collection<Foo>
ArrayList(…)
中以获得更具体的集合。是否有更好的方法,可能使用更通用的缩减操作
@Test
public void reduce() {
Collection<Foo> foos = Stream.of("foo", "bar", "baz")
.flatMap(this::getfoos)
.collect(Collectors.toMap(f -> f.name, f -> f, (l, r) -> {
l.ids.addAll(r.ids);
return l;
})).values();
assertEquals(3, foos.size());
foos.forEach(f -> assertEquals(10, f.ids.size()));
}
private Stream<Foo> getfoos(String n) {
return IntStream.range(0,10).mapToObj(i -> new Foo(n, i));
}
public static class Foo {
private String name;
private List<Integer> ids = new ArrayList<>();
public Foo(String n, int i) {
name = n;
ids.add(i);
}
}
@测试
公共空间减少(){
集合foos=流(“foo”、“bar”、“baz”)
.flatMap(this::getfoos)
.collect(收集器.toMap(f->f.name,f->f,(l,r)->{
l、 addAll(r.ids);
返回l;
})).values();
assertEquals(3,foos.size());
forEach(f->assertEquals(10,f.ids.size());
}
私有流getfoos(字符串n){
返回IntStream.range(0,10).mapToObj(i->newfoo(n,i));
}
公共静态类Foo{
私有字符串名称;
私有列表ID=new ArrayList();
公共Foo(字符串n,int i){
name=n;
同上,添加(i);
}
}
如果您打破分组并减少步骤,您可以得到更干净的东西:
Stream<Foo> input = Stream.of("foo", "bar", "baz").flatMap(this::getfoos);
Map<String, Optional<Foo>> collect = input.collect(Collectors.groupingBy(f -> f.name, Collectors.reducing(Foo::merge)));
Collection<Optional<Foo>> collected = collect.values();
正如在评论中已经指出的,当您想要识别独特的对象时,使用地图是非常自然的事情。如果您只需要找到唯一的对象,那么可以使用
Stream::distinct
方法。此方法隐藏了一个事实,即涉及到一个映射,但显然它在内部使用了一个映射,这表明您应该实现一个hashCode
方法,或者distinct
可能行为不正确
对于distinct
方法,如果不需要合并,则可以在处理所有输入之前返回部分结果。在您的情况下,除非您可以对问题中未提及的输入进行其他假设,否则您确实需要在返回任何结果之前完成所有输入的处理。因此,这个答案确实使用了地图
不过,使用streams处理映射的值并将其转换回ArrayList非常简单。我在这个答案中说明了这一点,并提供了一种避免出现可选
的方法,该选项出现在其他答案之一中
public void reduce() {
ArrayList<Foo> foos = Stream.of("foo", "bar", "baz").flatMap(this::getfoos)
.collect(Collectors.collectingAndThen(Collectors.groupingBy(f -> f.name,
Collectors.reducing(Foo.identity(), Foo::merge)),
map -> map.values().stream().
collect(Collectors.toCollection(ArrayList::new))));
assertEquals(3, foos.size());
foos.forEach(f -> assertEquals(10, f.ids.size()));
}
private Stream<Foo> getfoos(String n) {
return IntStream.range(0, 10).mapToObj(i -> new Foo(n, i));
}
public static class Foo {
private String name;
private List<Integer> ids = new ArrayList<>();
private static final Foo BASE_FOO = new Foo("", 0);
public static Foo identity() {
return BASE_FOO;
}
// use only if side effects to the argument objects are okay
public static Foo merge(Foo fooOne, Foo fooTwo) {
if (fooOne == BASE_FOO) {
return fooTwo;
} else if (fooTwo == BASE_FOO) {
return fooOne;
}
fooOne.ids.addAll(fooTwo.ids);
return fooOne;
}
public Foo(String n, int i) {
name = n;
ids.add(i);
}
}
public void reduce(){
ArrayList foos=Stream.of(“foo”、“bar”、“baz”).flatMap(this::getfoos)
.collect(收集器.collecting)然后(收集器.groupingBy(f->f.name),
reduceing(Foo.identity(),Foo::merge)),
map->map.values().stream()。
collect(Collectors.toCollection(ArrayList::new));
assertEquals(3,foos.size());
forEach(f->assertEquals(10,f.ids.size());
}
私有流getfoos(字符串n){
返回IntStream.range(0,10).mapToObj(i->newfoo(n,i));
}
公共静态类Foo{
私有字符串名称;
私有列表ID=new ArrayList();
私有静态final Foo BASE_Foo=new Foo(“,0);
公共静态Foo标识(){
返回基地(u FOO);;
}
//仅在参数对象的副作用正常时使用
公共静态Foo合并(Foo fooOne,Foo fooTwo){
如果(fooOne==BASE\u FOO){
返回fooTwo;
}否则如果(fooTwo==BASE\u FOO){
返回fooOne;
}
fooOne.ids.addAll(fooTwo.ids);
返回fooOne;
}
公共Foo(字符串n,int i){
name=n;
同上,添加(i);
}
}
如果输入元素是以随机顺序提供的,那么使用中间映射可能是最好的解决方案。但是,如果您事先知道具有相同名称的所有foo都是相邻的(在您的测试中实际上满足了此条件),则可以大大简化算法:您只需将当前元素与前一个元素进行比较,并在名称相同的情况下合并它们
不幸的是,没有流API方法可以让您轻松有效地完成这类工作。一种可能的解决方案是编写如下自定义收集器:
public static List<Foo> withCollector(Stream<Foo> stream) {
return stream.collect(Collector.<Foo, List<Foo>>of(ArrayList::new,
(list, t) -> {
Foo f;
if(list.isEmpty() || !(f = list.get(list.size()-1)).name.equals(t.name))
list.add(t);
else
f.ids.addAll(t.ids);
},
(l1, l2) -> {
if(l1.isEmpty())
return l2;
if(l2.isEmpty())
return l1;
if(l1.get(l1.size()-1).name.equals(l2.get(0).name)) {
l1.get(l1.size()-1).ids.addAll(l2.get(0).ids);
l1.addAll(l2.subList(1, l2.size()));
} else {
l1.addAll(l2);
}
return l1;
}));
}
此方法接受两个参数:应用于两个相邻元素的BiPredicate
,如果要合并元素,则应返回true;执行合并的BinaryOperator
。在顺序模式下,此解决方案比自定义收集器稍慢一点(并行时,结果非常相似),但它仍然比toMap
解决方案快得多,而且它更简单,更灵活,因为collapse
是一种中间操作,因此您可以用另一种方式进行收集
同样,只有当已知同名的foo相邻时,这两种解决方案才有效。按foo名称对输入流进行排序是个坏主意,然后使用这些解决方案,因为排序会大大降低性能,使其比
toMap
解决方案慢。正如其他人已经指出的那样,中间映射是不可避免的,因为这是查找要合并的对象的方法。此外,在缩减期间不应修改源数据
不过,您可以在不创建多个Foo
实例的情况下实现这两个目标:
List<Foo> foos = Stream.of("foo", "bar", "baz")
.flatMap(n->IntStream.range(0,10).mapToObj(i -> new Foo(n, i)))
.collect(collectingAndThen(groupingBy(f -> f.name),
m->m.entrySet().stream().map(e->new Foo(e.getKey(),
e.getValue().stream().flatMap(f->f.ids.stream()).collect(toList())))
.collect(toList())));
List foos=Stream.of(“foo”、“bar”、“baz”)
.flatMap(n->IntStream.range(0,10).mapToObj(i->newfoo(n,i)))
.collect(collecting)然后(groupingBy(f->f.name),
m->m.entrySet().stream().map(e->newfoo(e.getKey(),
e、 getValue().stream().flatMap(f->f.ids.stream()).collect(toList()))
.收集(toList());
这假设您添加了一个构造函数
public Foo(String n, List<Integer> l) {
name = n;
ids=l;
}
public Foo(字符串n,列表l){
name=n;
ids=l;
}
List<Foo> foos = Stream.of("foo", "bar", "baz")
.flatMap(n->IntStream.range(0,10).mapToObj(i -> new Foo(n, i)))
.collect(collectingAndThen(groupingBy(f -> f.name),
m->m.entrySet().stream().map(e->new Foo(e.getKey(),
e.getValue().stream().flatMap(f->f.ids.stream()).collect(toList())))
.collect(toList())));
public Foo(String n, List<Integer> l) {
name = n;
ids=l;
}