Java 分组并减少对象列表

Java 分组并减少对象列表,java,java-8,Java,Java 8,我有一个对象列表,其中有许多重复的对象和一些需要合并的字段。我想将其简化为仅使用Java8流的唯一对象列表(我知道如何通过旧的skool方法实现这一点,但这只是一个实验) 这就是我现在拥有的。我真的不喜欢这样,因为映射构建似乎是无关的,values()集合是支持映射的视图,您需要将其包装在新的ArrayList(…)中以获得更具体的集合。是否有更好的方法,可能使用更通用的缩减操作 @Test public void reduce() { Collection<Foo>

我有一个对象列表,其中有许多重复的对象和一些需要合并的字段。我想将其简化为仅使用Java8流的唯一对象列表(我知道如何通过旧的skool方法实现这一点,但这只是一个实验)

这就是我现在拥有的。我真的不喜欢这样,因为映射构建似乎是无关的,values()集合是支持映射的视图,您需要将其包装在新的
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;
    }