I don't think there is a way to get around the map. You can however use groupingBy
to hide it, but it is effectively the same code and performance as Paul Boddington suggested.
List<A> merge(List<A> input) {
return input.stream()
.collect(groupingBy(a -> a.name)) // map created here
.entrySet()
.stream()
.map(entry -> new A(
entry.getKey(),
entry.getValue().stream()
.flatMap(list -> list.numbers.stream())
.collect(toList()) // merging behaviour
)).collect(toList());
}
There is no mutation of original list and you can easily change the behaviour of merging the lists - for example if you want to get rid of duplicates just add .distinct()
after flatMap(list -> list.numbers.stream())
(remember about adding equals
to B
) or in a similar way you can sort them by just adding .sorted()
(you have to make B implement Comparable
interface or just use .sorted(Comparator<B>)
).
Here is full code with tests and imports:
import org.junit.Test;
import java.util.List;
import static com.shazam.shazamcrest.MatcherAssert.assertThat;
import static com.shazam.shazamcrest.matcher.Matchers.sameBeanAs;
import static java.util.Arrays.asList;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
public class Main {
class A {
final String name;
final List<B> numbers;
A(String name, List<B> numbers) {
this.name = name;
this.numbers = numbers;
}
}
class B {
final Integer number;
B(Integer number) {
this.number = number;
}
}
List<A> merge(List<A> input) {
return input.stream()
.collect(groupingBy(a -> a.name))
.entrySet()
.stream()
.map(entry -> new A(
entry.getKey(),
entry.getValue().stream()
.flatMap(list -> list.numbers.stream())
.collect(toList())
)).collect(toList());
}
@Test
public void test() {
List<A> input = asList(
new A("abc", asList(new B(1), new B(2))),
new A("xyz", asList(new B(3), new B(4))),
new A("abc", asList(new B(3), new B(5)))
);
List<A> list = merge(input);
assertThat(list, sameBeanAs(asList(
new A("abc", asList(new B(1), new B(2), new B(3), new B(5))),
new A("xyz", asList(new B(3), new B(4)))
)));
}
}
EDIT:
Following your questions in the comments, if you want to add multiple fields into groupingBy
clause, you would need to create a class that represents a key in the map. If you have fields that you don't want to include in the key, then you have to define how to merge two different values - similarly to what you do with numbers. Depending on what the fields are, merging behaviour can be just choosing the first value and discarding the other (what I have done with numbers
in the code below).
class A {
final String name;
final String type;
final List<B> numbers;
A(String name, String type, List<B> numbers) {
this.name = name;
this.type = type;
this.numbers = numbers;
}
}
class B {
final Integer number;
B(Integer number) {
this.number = number;
}
}
class Group {
final String name;
final String type;
Group(String name, String type) {
this.name = name;
this.type = type;
}
// this needs to have equals and hashCode methods as we use it as a key in a map
}
List<A> merge(List<A> input) {
return input.stream()
.collect(groupingBy(a -> new Group(a.name, a.type)))
.entrySet()
.stream()
.map(entry -> new A(
entry.getKey().name, // entry.getKey() is now Group, not String
entry.getKey().type,
entry.getValue().get(0).numbers // no merging, just take first
)).collect(toList());
}