简体   繁体   English

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

[英]Merging lists under same objects in a list using Java streams

I have two objects like following:我有两个对象如下:

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
}

and

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

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

    //getters and setters
}

So, A holds a list of B and there is a list of A populated as follows:因此,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"))));

I want to acquire a new list by merging A objects with same ids.我想通过合并具有相同 ID 的 A 对象来获取一个新列表。 Result list must be like that:结果列表必须是这样的:

[
    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)])
]

I did manage to merge the list with the following code:我确实设法将列表与以下代码合并:

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

My question is, is there any proper way to do this by using stream collectors?我的问题是,是否有任何正确的方法可以通过使用流收集器来做到这一点?

Assuming class A has a copy constructor that effectively copies the List<B> list attribute and a method that merges two instances of A :假设类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;
}

You could achieve what you want as follows:你可以实现你想要的如下:

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

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

This uses Collectors.toMap , which expects a function that extracts the key of the map from the elements of the stream (here this would be A::getId , which extracts the id of A ), a function that transforms each element of the stream to the values of the map (here it would be A::new , which references the copy constructor) and a merge function that combines two values of the map that have the same key (here this would be A::merge , which is only called when the map already contains an entry for the same key).这使用Collectors.toMap ,它需要一个函数从流的元素中提取映射的键(这里是A::getId ,它提取Aid ),一个转换流的每个元素的函数到地图的值(这里是A::new ,它引用复制构造函数)和一个合并函数,它结合了具有相同键的地图的两个值(这里是A::merge ,这是仅当映射已经包含相同键的条目时调用)。

If you need a List<A> instead of a Collection<A> , simply do:如果您需要List<A>而不是Collection<A> ,只需执行以下操作:

List<A> result = new ArrayList<>(map.values());
Collection<A> merge(List<A> list) {
    return list.stream()
            .collect(Collectors.toMap(a -> a.id, Function.identity(), this::merge))
            .values();
}

A merge(A a1, A a2) {
    if (!a1.name.equals(a2.name)) {
        throw new IllegalArgumentException("We assumed same id means same name");
    }
    return new A(a1.id, a1.name, union(a1.list, a2.list));
}

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

If you can use vanilla Java here is a very easy solution.如果你可以在这里使用 vanilla Java 是一个非常简单的解决方案。 The list is iterated at once.该列表会立即迭代。

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

If you don't want to use extra functions you can do the following, it's readable and easy to understand, first group by id, create a new object with the first element in the list and then join all the B's classes to finally collect the A's.如果你不想使用额外的功能,你可以执行以下操作,它可读且易于理解,首先按 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());

Another way is to use a binary operator and the Collectors.groupingBy method.另一种方法是使用二元运算符和Collectors.groupingBy方法。 Here you use the java 8 optional class to create the new A the first time when fst is null.在这里,当fst为 null 时,您第一次使用 java 8 可选类创建新的 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();

If you don't like to use return in short lambdas (doesn't look that well) the only option is a filter because java does not provide another method like stream's peek (note: some IDEs highlight to 'simplify' the expression and mutations shouldn't be made in filter [but i think in maps neither]).如果您不喜欢在短 lambda 中使用 return(看起来不太好),唯一的选择是过滤器,因为 java 不提供另一种方法,如流的 peek(注意:一些 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())));

You can also use this joiner as a generic method and create a left to right reducer with a consumer that allows to join the new mutable object created with the initializer function.您还可以将此连接器用作通用方法,并创建一个带有消费者的从左到右的缩减器,该消费者允许连接使用初始化函数创建的新可变对象。

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

And use it like并像使用它

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

Or like if you have an empty constructor that initializes the collections或者如果你有一个空的构造函数来初始化集合

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

Finally, if you already have the copy constructor or the merge method mentioned in the other answers you can simplify the code even more or use the Collectors.toMap method.最后,如果您已经有了其他答案中提到的复制构造函数或合并方法,您可以进一步简化代码或使用Collectors.toMap方法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM