Search code examples
javalistjava-8java-streamgroupingby

How to Group a List of objects by Two fields and obtain a List of objects of resulting type


I have a list of objects shown below:

public class AssignDTO {    
    private String assignmentIbanId;    
    private String agreementFileId;    
    private BankData bankData;

   // getter and setter
}

[
   {
      "assignmentIbanId":"6146dd87-2344-4149-9396-8e8a88493dd6",
      "agreementFileId":"36de8604-ae56-49b3-b972-8071b459bf82",
      "bankData": { "useType" : "a" }
   },
   {
      "assignmentIbanId":"44cb8cc6-fe28-4e44-a78c-48a908795f38",
      "agreementFileId":"854f0c37-b9d8-4533-9c02-0c1b64a909bd",
      "bankData": { "useType" : "b" }
   },
   {
      "assignmentIbanId":"44cb8cc6-fe28-4e44-a78c-48a908795f38",
      "agreementFileId":"854f0c37-b9d8-4533-9c02-0c1b64a909bd",
      "bankData": { "useType" : "c" }
   }
]

How can I group the list by agreementFileId and agreementFileId fields like below

public class ResultDTO {    
    private String assignmentIbanId;    
    private String agreementFileId;    
    private Set<BankData> bankAccounts;

   // getter and setter
}

[
   {
      "assignmentIbanId":"6146dd87-2344-4149-9396-8e8a88493dd6",
      "agreementFileId":"36de8604-ae56-49b3-b972-8071b459bf82",
      "bankAccounts": [{ "useType" : "a" }]
   }
   {
      "assignmentIbanId":"44cb8cc6-fe28-4e44-a78c-48a908795f38",
      "agreementFileId":"854f0c37-b9d8-4533-9c02-0c1b64a909bd",
      "bankAccounts": [{ "useType" : "b" }, { "useType" : "c" }]
   }
]

Solution

  • As the first step, you might either create a nested map applying the Collector.groupingBy() twice, or group the data using an object that is capable to carry two values, like a Map.Entry, as a key.

    In both cases, a combination of mapping() and toSet() would be handy as a downstream collector:

    Collectors.mapping(AssignDTO::getBankData, Collectors.toSet()
    

    As a result, it'll produce an intermediate map having Set<BankData> as its values.

    Then create a stream over the entry set and apply map() to turn each entry into a ResultDTO and collect the result into a list either with toList() (Java 16+) or by utilizing Collectors.toList().

    That's how it might look like (example with map entry for Java-8):

    public static void main(String[] args) {
        List<AssignDTO> source =
            List.of(new AssignDTO("6146dd87-2344-4149-9396-8e8a88493dd6",
                        "36de8604-ae56-49b3-b972-8071b459bf82", new BankData("a")),
                    new AssignDTO("44cb8cc6-fe28-4e44-a78c-48a908795f38",
                        "854f0c37-b9d8-4533-9c02-0c1b64a909bd", new BankData("b")),
                    new AssignDTO("44cb8cc6-fe28-4e44-a78c-48a908795f38",
                        "854f0c37-b9d8-4533-9c02-0c1b64a909bd", new BankData("c")));
        
        List<ResultDTO> result =
            source.stream()
                .collect(Collectors.groupingBy(assignDTO -> new AbstractMap.SimpleEntry<>(assignDTO.getAgreementFileId(), assignDTO.getAgreementFileId()),
                            Collectors.mapping(AssignDTO::getBankData, Collectors.toSet()))) // creating an intermediate map `Map<Map.Entry<String, String>>, Set<BankData>>
                .entrySet().stream()
                .map(entry -> new ResultDTO(entry.getKey().getKey(),
                                            entry.getKey().getValue(),
                                            entry.getValue()))
                .collect(Collectors.toList());
    
        result.forEach(System.out::println);
    }
    

    Output

    ResultDTO{
        assignmentIbanId : '854f0c37-b9d8-4533-9c02-0c1b64a909bd',
        agreementFileId : '854f0c37-b9d8-4533-9c02-0c1b64a909bd',
        bankAccounts : [{useType : 'b'}, {useType : 'c'}]}
    ResultDTO{
        assignmentIbanId : '36de8604-ae56-49b3-b972-8071b459bf82',
        agreementFileId : '36de8604-ae56-49b3-b972-8071b459bf82',
        bankAccounts : [{useType : 'a'}]}