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:
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. 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?
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.
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. Here you use the java 8 optional class to create the new A the first time when fst is null.
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]).
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.