I have two objects like following:
public class A {
private Integer id;
private String name;
private List list;
public A(Integer id
Assuming class A
has a copy constructor that effectively copies the List<B> list
attribute and a method that merges two instances of 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).
If you need a List<A>
instead of a Collection<A>
, simply do:
List<A> result = new ArrayList<>(map.values());
If you can use vanilla Java here is a very easy solution. 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.
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.
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;
}