Home > OS >  Create a map from a 2 level nested list where key is part of the nested list's object?
Create a map from a 2 level nested list where key is part of the nested list's object?

Time:10-13

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:1, name:"Hi"}, B{id:1, name:"Bye"}, B{id:1, name:"Yes"}],
classBList=[B{id:2, name:"No"}, B{id:2, name:"Go"}, B{id:2, name:"Yellow"}],
classBList=[B{id:2, name:"Joe"}, B{id:2, name:"Blow"}]]

(3 elements in a list of A objects: 4 elements in classBList for first element, 3 elements in classBList for second element, 2 elements in classBList for third element)

Each B element in a classBList will have the same id.

Output:

{Key=1, Value=[ A{classBList=[B{id:1, name:"Hello"}, B{id:1, name:"Hi"}, B{id:1, name:"Bye"}, B{id:1, name:"Yes"}]} ]

{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"}]} ]

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.

CodePudding user response:

You'll need to flat map to some sort of tuple class, like AbstractMap.SimpleEntry, so you can stream A and B in parallel and then invert the grouping:

classAListInput.stream()
        .flatMap(a -> a.getClassBList()
                .stream()
                .map(b -> new SimpleEntry<>(b.getId(), a))
        .collect(groupingBy(Entry::getKey, mapping(Entry::getValue, toList())))

CodePudding user response:

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
        )
    ));

CodePudding user response:

If I understood the problem correctly, judging by the sample data, you have a List<List<B>> as an input.

And based on the sample output you've provided, you need to obtain a map of type Map<Integer,A> as a result (not a Map<Integer,List<A>>).

This can be done in the following steps:

  • flatten the data using flatMap(), i.e. transform a Stream<List<B>> into a Stream<B>;
  • group the elements by id by the means of collector groupingBy();
  • collect the elements mapped to the same key into a list and transform them into an object A, which can be done by applying a combination of collectors collectingAndThen() and toList() as the downstream of groupingBy().

That's how it might be implemented:

public static void main(String[] args) {
    List<List<B>> classAListInput = List.of(
        List.of(new B(1, "Hello"), new B(1, "Hi"), new B(1, "Bye"), new B(1, "Yes")),
        List.of(new B(2, "No"), new B(2, "Go"), new B(2, "Yellow")),
        List.of(new B(2, "Joe"), new B(2, "Blow"))
    );
    
    Map<Integer, A> aById = classAListInput.stream()
        .flatMap(Collection::stream)      // flattening the data
        .collect(Collectors.groupingBy(
            B::getId,                     // grouping by id
            Collectors.collectingAndThen(
                Collectors.toList(),      // accumulating elements into a list
                A::new)                   // instantiating object A based on the List<B>
        ));
    
    aById.forEach((id, a) -> System.out.println(id   " -> "   a));
}

Output:

1 -> A{classBList=[B{id=1, name='Hello'}, B{id=1, name='Hi'}, B{id=1, name='Bye'}, B{id=1, name='Yes'}]}
2 -> 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'}]}

A link to Online Demo

  • Related