Search code examples
javalistjava-streamjava-11collectors

Grouping a list of Flattened Objects into a complex Nested structure


I have a List of objects A, where the object A has this form:

class A {
    private String a1;
    private String a2;
    private String a3;
    private String a4;
    private String a5;
    private String a6;
}

I need to group this List first by a1 and a2, then by a3 and a4, resulting into List<B>

Where the object B has this form

class B {
    private String a1; 
    private String a2;
    private List<C> list;
}

And where the object C has this form

class C {
    private String a3;
    private String a4;
    private List<D> list;
}

And where the object D has this form

class D {
    private String a5;
    private String a6;
}

Example. Given the list:

  [{a1="100", a2="bbb", a3="100100", a4="ddd", a5="1", a6="10"},
   {a1="100", a2="bbb", a3="100100", a4="ddd", a5="2", a6="20"},
   {a1="100", a2="bbb", a3="100200", a4="eee", a5="3", a6="30"},
   {a1="200", a2="ccc", a3="200100", a4="fff", a5="4", a6="40"},
   {a1="200", a2="ccc", a3="200200", a4="ggg", a5="5", a6="50"},
   {a1="200", a2="ccc", a3="200300", a4="hhh", a5="6", a6="60"}]

I need this structure as a result:

    {"B": [
    {
      "a1": "100",
      "a2": "bbb",
      "C": [
        {
          "a3": "100100",
          "a4": "ddd",
          "D": [
            {"a5": "1", "a6": "10"},
            {"a5": "2", "a6": "20"}
          ]
        },
        {
          "a3": "100200",
          "a4": "eee",
          "D": [
            {"a5": "3", "a6": "30"}
          ]
        }
      ]
    },
     {
      "a1": "200",
      "a2": "ccc",
      "C": [
        {
          "a3": "200100",
          "a4": "fff",
          "D": [
            {"a5": "4", "a6": "40"}
          ]
        },
        {
          "a3": "200200",
          "a4": "ggg",
          "D": [
            {"a5": "5", "a6": "50"}
          ]
        },
        {
          "a3": "200300",
          "a4": "hhh",
          "D": [
            {"a5": "6", "a6": "60"}
          ]
        }
      ]
    }
  ]
}

Is this possible with streams in Java 11? Any other ways to achieve this goal are welcome as well.


Solution

  • The code provided below does what required.

    There's a problem though which derived from the way of how your data is structured:

    • If chunks of data like pairs of strings a1 and a2, a3 and a4, etc. constitute self-contained units of information that have a particular meaning in your domain model, you shouldn't create such beasts like A with huge a number of string fields (object A must contain a collection of objects B and that it). It's a faulty design, you should refine you classes.
    • Some strings in your example contain numeric data. If you are parsing it somewhere later on, that isn't good as well. They must be replaced with numeric data types.

    The solution below makes use of built-in collectors Collector.collectionAndThen() and Collectors.groupingBy(). Map.Entry is utilized as an intermediate container of data in both groupingBy() collectors (Map.Entry objects are keys in both maps).

    Functions are responsible for transforming these maps into list List<C> and List<B>.

    public static void main(String[] args) {
        List<A> aList = List.of(new A("100", "bbb", "100100", "ddd", "1", "10"),
                                new A("100", "bbb", "100100", "ddd", "2", "20"),
                                new A("100", "bbb", "100200", "eee", "3", "30"),
                                new A("200", "ccc", "200100", "fff", "4", "40"),
                                new A("200", "ccc", "200200", "ggg", "5", "50"),
                                new A("200", "ccc", "200300", "hhh", "6", "60"));
        
        Function<Map<Map.Entry<String, String>, List<D>>, List<C>> mapToListC =
            mapC ->  mapC.entrySet().stream()
                    .map(entry -> new C(entry.getKey().getKey(),
                                        entry.getKey().getValue(),
                                        entry.getValue()))
                    .collect(Collectors.toList());
    
        Function<Map<Map.Entry<String, String>, Map<Map.Entry<String, String>, List<D>>>, List<B>> mapToListB =
            mapB ->  mapB.entrySet().stream()
                .map(entry -> new B(entry.getKey().getKey(),
                                    entry.getKey().getValue(),
                                    mapToListC.apply(entry.getValue())))
                .collect(Collectors.toList());
    
        List<B> bList = aList.stream()
            .collect(Collectors.collectingAndThen(
                    Collectors.groupingBy((A a) -> Map.entry(a.getA1(), a.getA2()),
                        Collectors.groupingBy((A a) -> Map.entry(a.getA3(), a.getA4()),
                            Collectors.mapping((A a) -> new D(a.getA5(), a.getA6()),
                                Collectors.toList()))),
                    mapToListB));
        
        bList.forEach(System.out::println);
    }
    

    Output

    B{a1='200', a2='ccc'list=
        C{a3='200300', a4='hhh'list=
            D{a5='6', a6='60'}}
        C{a3='200100', a4='fff'list=
            D{a5='4', a6='40'}}
        C{a3='200200', a4='ggg'list=
            D{a5='5', a6='50'}}}
    B{a1='100', a2='bbb'list=
        C{a3='100200', a4='eee'list=
            D{a5='3', a6='30'}}
        C{a3='100100', a4='ddd'list=
            D{a5='1', a6='10'}
            D{a5='2', a6='20'}}}