Reputation: 153
Given,
class Foo {
private Long id;
private String name;
private String category;
private List<String> categories;
// getters & setters
}
I have a list of objects.
final Foo f1 = new Foo(1L, "a", "c1");
final Foo f2 = new Foo(1L, "a", "c2");
final Foo f3 = new Foo(2L, "a", "c1");
final List<Foo> li = List.of(f1, f2, f3);
which looks like
{[Foo [id=1, name=a, category=c1, categories=null], Foo [id=1, name=a, category=c2, categories=null]], [Foo [id=2, name=a, category=c1, categories=null]]}
I want to transform this to
[Foo [id=1, name=a, category=null, categories=[c1, c2]], Foo [id=2, name=a, category=null, categories=[c1]]]
i.e. collate the individual category
into a list of categories
.
This is the current code that achieves what I want.
public static void main(final String[] args) {
final Foo f1 = new Foo(1L, "a", "c1");
final Foo f2 = new Foo(1L, "a", "c2");
final Foo f3 = new Foo(2L, "a", "c1");
final List<Foo> li = List.of(f1, f2, f3);
li.forEach(e -> System.out.println(e));
final Map<Long, List<Foo>> collect = li.stream().collect(Collectors.groupingBy(Foo::getId));
System.out.println(collect);
final List<Foo> grouped = new ArrayList<>();
collect.forEach((k, v) -> {
System.out
.println("key=" + k + "val=" + v.stream().map(e1 -> e1.getCategory()).collect(Collectors.toList()));
final Foo foo = collect.get(k).get(0);
foo.setCategories(v.stream().map(e1 -> e1.getCategory()).collect(Collectors.toList()));
foo.setCategory(null);
grouped.add(foo);
});
System.out.println(grouped);
}
Is there any way to do this using streams & lambdas alone without having to break into multiple steps? The goal is to make this code more elegant, readable, and convey the intention to the reader.
This question is similar in nature to Group by and sum objects like in SQL with Java lambdas? but didn't help me since there an aggregation is done whereas here it's not an aggregation.
Upvotes: 2
Views: 2414
Reputation: 19545
It can be done by implementing a merge
function which would accumulate the categories in the categories list, and then using reduce
operation of the stream:
class Foo {
static Foo merge(Foo accum, Foo other) {
if (null == accum.categories) {
accum.categories = new ArrayList<>();
if (null != accum.category) {
accum.categories.add(accum.category);
accum.category = null;
}
}
accum.categories.add(other.category);
return accum;
}
}
Implementation:
static List<Foo> joinedCategoriesReduce(List<Foo> input) {
return input
.stream()
.collect(Collectors.groupingBy(Foo::getId)) // Map<Integer, List<Foo>>
.values().stream() // Stream<List<Foo>>
.map(v -> v.stream() // Stream<Foo>
.reduce(new Foo(v.get(0).getId(), v.get(0).getName(), (String)null), Foo::merge)
)
.collect(Collectors.toList());
}
Test
final Foo f1 = new Foo(1L, "a", "c1");
final Foo f2 = new Foo(1L, "a", "c2");
final Foo f3 = new Foo(2L, "a", "c1");
final List<Foo> li = List.of(f1, f2, f3);
joinedCategoriesReduce(li).forEach(System.out::println);
Output
Foo(id=1, name=a, category=null, categories=[c1, c2])
Foo(id=2, name=a, category=null, categories=[c1])
Another option is to provide a Foo constructor accepting a list of categories:
// class Foo
public Foo(Long id, String name, List<String> cats) {
this.id = id;
this.name = name;
this.categories = cats;
}
Then the map entries could be easily remapped to Foo
instance with the list of categories:
static List<Foo> joinedCategoriesMap(List<Foo> input) {
return input
.stream()
.collect(Collectors.groupingBy(Foo::getId))
.values().stream()
.map(v -> new Foo(
v.get(0).getId(),
v.get(0).getName(),
v.stream().map(Foo::getCategory).collect(Collectors.toList()))
)
.collect(Collectors.toList());
}
Upvotes: 1