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

问题描述 投票:3回答:4

我有两个对象如下:

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);
    }
});

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

java java-8 java-stream collectors
4个回答
3
投票

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

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为null时第一次创建新的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方法。


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,它提取idA),这个函数将流的每个元素转换为值的map(这里是引用复制构造函数的A::new)和一个合并函数,它合并了具有相同键的映射的两个值(这里将是A::merge,仅当映射已经包含一个条目时才调用它相同的关键)。

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

List<A> result = new ArrayList<>(map.values());

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
投票
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;
}
© www.soinside.com 2019 - 2024. All rights reserved.