使用Java流合并列表中相同对象下的列表

时间:2018-03-05 21:19:33

标签: java java-8 java-stream collectors

我有两个对象如下:

public class A {
    private Integer id;
    private String name;
    private List<B> list;

    public A(Integer id, String name, List<B> list) {
        this.id = id;
        this.name = name;
        this.list = list;
    }

    //getters and setters
}

public class B {
    private Integer id;
    private String name;

    public B(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    //getters and setters
}

因此,A包含B的列表,并且有一个填充的A列表,如下所示:

    List<A> list = new ArrayList<>();
    list.add(new A(1, "a_one", Arrays.asList(new B(1, "b_one"), new B(2, "b_two"))));
    list.add(new A(2, "a_two", Arrays.asList(new B(2, "b_two"))));
    list.add(new A(1, "a_one", Arrays.asList(new B(3, "b_three"))));
    list.add(new A(2, "a_two", Arrays.asList(new B(4, "b_four"), new B(5, "b_five"))));
    list.add(new A(3, "a_three", Arrays.asList(new B(4, "b_four"), new B(5, "b_five"))));

我想通过合并具有相同ID的对象来获取新列表。结果列表必须如下:

[
    A(1, a_one, [B(1, b_one), B(2, b_two), B(3, b_three)]),
    A(2, a_two, [B(2, b_two), B(4, b_four), B(5, b_five)]),
    A(3, a_three, [B(4, b_four), B(5, b_five)])
]

我确实设法将列表与以下代码合并:

List<A> resultList = new ArrayList<>();
list.forEach(a -> {
    if (resultList.stream().noneMatch(ai -> ai.getId().equals(a.getId()))) {
        a.setList(list.stream().filter(ai -> ai.getId().equals(a.getId()))
                .flatMap(ai -> ai.getList().stream()).collect(Collectors.toList()));
        resultList.add(a);
    }
});

我的问题是,有没有正确的方法来使用流收集器?

4 个答案:

答案 0 :(得分:4)

假设类A有一个有效复制List<B> list属性的复制构造函数和一个合并两个A实例的方法:

public A(A another) {
    this.id = another.id;
    this.name = another.name;
    this.list = new ArrayList<>(another.list);
}

public A merge(A another) {
    list.addAll(another.list):
    return this;
}

您可以按照以下方式实现目标:

Map<Integer, A> result = listOfA.stream()
    .collect(Collectors.toMap(A::getId, A::new, A::merge));

Collection<A> result = map.values();

这使用Collectors.toMap,它需要一个从流的元素中提取地图关键字的函数(此处为A::getId,它提取id的{​​{1}} 1}}),一个函数,它将流的每个元素转换为映射的值(这里它将是引用复制构造函数的A)和一个合并函数,它将两个映射值组合在一起相同的密钥(此处为A::new,仅在地图已包含相同密钥的条目时才会调用。

如果您需要A::merge而不是List<A>,请执行以下操作:

Collection<A>

答案 1 :(得分:3)

如果您可以使用vanilla Java,这是一个非常简单的解决方案。列表会立即迭代。

Map<Integer, A> m = new HashMap<>();
for (A a : list) {
    if (m.containsKey(a.getId()))
        m.get(a.getId()).getList().addAll(a.getList());
    else
         m.put(a.getId(), new A(a.getId(), a.getName(), a.getList()));
}
List<A> output =  new ArrayList<>(m.values());

答案 2 :(得分:3)

如果您不想使用额外的功能,您可以执行以下操作,它可读且易于理解,首先按ID分组,使用列表中的第一个元素创建一个新对象,然后将所有B的类连接到最后收集A的。

List<A> result = list.stream()
    .collect(Collectors.groupingBy(A::getId))
    .values().stream()
    .map(grouped -> new A(grouped.get(0).getId(), grouped.get(0).getName(),
            grouped.stream().map(A::getList).flatMap(List::stream)
                .collect(Collectors.toList())))
    .collect(Collectors.toList());

另一种方法是使用二元运算符和Collectors.groupingBy方法。这里使用java 8可选类在 fst 为空时第一次创建新的A.

BinaryOperator<A> joiner = (fst, snd) -> Optional.ofNullable(fst)
    .map(cur -> { cur.getList().addAll(snd.getList()); return cur; })
    .orElseGet(() -> new A(snd.getId(), snd.getName(), new ArrayList<>(snd.getList())));

Collection<A> result = list.stream()
    .collect(Collectors.groupingBy(A::getId, Collectors.reducing(null, joiner)))
    .values();

如果你不喜欢在短lambda中使用return(看起来不太好),唯一的选择就是过滤器,因为java没有提供像stream一样的另一种方法(注意:有些IDE突出显示'简化'表达和突变不应该在过滤器中进行[但我认为在地图中都没有])。

BinaryOperator<A> joiner = (fst, snd) -> Optional.ofNullable(fst)
    .filter(cur -> cur.getList().addAll(snd.getList()) || true)
    .orElseGet(() -> new A(snd.getId(), snd.getName(), new ArrayList<>(snd.getList())));

您还可以将此joiner用作通用方法,并使用允许加入使用初始化函数创建的新可变对象的使用者创建一个从左到右的reducer。

public class Reducer {
    public static <A> Collector<A, ?, A> reduce(Function<A, A> initializer, 
                                                BiConsumer<A, A> combiner) {
        return Collectors.reducing(null, (fst, snd) -> Optional.ofNullable(fst)
            .map(cur -> { combiner.accept(cur, snd); return cur; })
            .orElseGet(() -> initializer.apply(snd)));
    }
    public static <A> Collector<A, ?, A> reduce(Supplier<A> supplier, 
                                                BiConsumer<A, A> combiner) {
        return reduce((ign) -> supplier.get(), combiner);
    }
}

并像

一样使用它
Collection<A> result = list.stream()
    .collect(Collectors.groupingBy(A::getId, Reducer.reduce(
        (cur) -> new A(cur.getId(), cur.getName(), new ArrayList<>(cur.getList())),
        (fst, snd) -> fst.getList().addAll(snd.getList())
    ))).values();

或者如果你有一个初始化集合的空构造函数

Collection<A> result = list.stream()
    .collect(Collectors.groupingBy(A::getId, Reducer.reduce(A::new,
        (fst, snd) -> {
            fst.getList().addAll(snd.getList());
            fst.setId(snd.getId());
            fst.setName(snd.getName());
        }
    ))).values();

最后,如果您已经拥有其他答案中提到的复制构造函数或合并方法,则可以进一步简化代码或使用Collectors.toMap方法。

答案 3 :(得分:2)

private Collection<A> merge(List<A> list) {
    return list.stream()
            .collect(Collectors.toMap(A::getId, Function.identity(), this::merge))
            .values();
}

private A merge(A a1, A a2) {
    // assume same id means same name
    return new A(a1.getId(), a1.getName(), union(a1.getList(), a2.getList()));
}

private List<B> union(List<B> l1, List<B> l2) {
    List<B> result = new ArrayList<>(l1);
    result.addAll(l2);
    return result;
}