I have a simple nested structure as such:
public static class A {
private List<B> classBList;
// constructor, getters, etc.
}
public static class B {
private int id;
private String name;
// constructor, getters, etc.
}
I want to create a map of <Integer,List<A>>
where the integer field in class B
id
will be the key, and the A
objects in the input that contain the matching id
will be rolled up into a list as the value. The input would be a list of class A
.
So for example:
Input:
[classBList=[B{id:1, name:"Hello"}, B{id:2, name:"Hi"}],
classBList=[B{id:3, name:"No"}, B{id:3, name:"Go"}],
classBList=[B{id:1, name:"Yes"}]]
Output:
{Key=1, Value=[ A{classBList=[B{id:1, name:"Hello"}, B{id:1, name:"Yes"}]} ]
{Key=2, Value=[ A{classBList=[B{id:2, name:"Hi"}]} ]
{Key=3, Value=[ A{classBList=[B{id:3, name:"No"}, B{id:3, name:"Go"}]} ]
I'm having trouble, however, writing the lambdas that allow this to happen. What I tried:
Map<Integer, List<A>> heyThere = classAListInput.stream()
.collect(Collectors.toMap(
A::getClass,
element -> element.getClassBList().stream()
.map(B::getId)
.collect(Collectors.toList())
));
But this doesn't compile, so really not sure of how the syntax should look.
If you're wondering why not just alter the map so it's <Integer, List< B >>, there are other fields in class A that I didn't note but would be needed in the output, so that's why a list of A objects would be the value in the map.
It seems that you need to rebuild instances of A
class with the new list of B
.
However, expected output shows that there is only one A
entry in the list, and all B
's are added to the same A
instance:
{Key=2, Value=[ A{classBList=[B{id:2, name:"No"}, B{id:2, name: "Go"}, B{id:2, name:"Yellow"}, B{id:2, name:"Joe"}, B{id:2, name:"Blow"}]} ]
So, the following implementation may be offered assuming there's an all-args constructor in A
class accepting List<B>
:
Map<Integer, List<A>> result = classAListInput
.stream() // Stream<A>
.flatMap(a -> a.getClassBList().stream()) // Stream<B>
.collect(Collectors.groupingBy(
B::getId,
Collectors.collectingAndThen(
Collectors.toList(), // List<B> flattening all B instances by id
lst -> List.of(new A(lst)) // or Arrays.asList or Collections.singletonList
)
));
Update
As the copies of A
class may need to be created with some additional fields in A
class and the filtered list of B
instances, the following change may be offered using Map.entry
(available since Java 9 which has been around for a while).
Also, to address an issue of duplicated entries for the same bKey
inside the same A
instance, Stream::distinct
operation should be applied after mapping to the key field:
Map<Integer, List<A>> aByBKey = classAListInput.stream()
.flatMap(a -> a.getClassBList()
.stream()
.map(B::getBKey)
.distinct()
.map(bk -> Map.entry(bk, getCopy(a, bk)) )
)
.collect(Collectors.groupingBy(
Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue, Collectors.toList())
));
aByBKey.forEach((bKey, a) -> System.out.println(bKey + " -> " + a));
where getCopy
is a static method constructing a copy instance of A
keeping the code of class A
intact:
public static A getCopy(A a, int bKey) {
return new A(
a.aKey,
a.classBList
.stream()
.filter(b -> b.getBKey() == bKey)
.collect(Collectors.toList())
);
}