Search code examples
javajava-streammappingcollectorsgroupingby

Java-Stream - Create a List of aggregated Objects using Collector groupingBy


I have a domain class View:

public class View {
    private String id;
    private String docId;
    private String name;
    
    // constructor, getters, etc.
}

And there's a list of View objects.

Elements having the same id, only differ in one field docId (the second attribute), example:

List<View> viewList = new ArrayList<>();
viewList.add(new View("1234", "ab123", "john"));
viewList.add(new View("1234", "cd456", "john"));
viewList.add(new View("1234", "ef789", "john"));
viewList.add(new View("5678", "jh987", "jack"));
viewList.add(new View("5678", "ij654", "jack"));
viewList.add(new View("5678", "kl321", "jack"));
viewList.add(new View("9876", "mn123", "ben"));
viewList.add(new View("9876", "op456", "ben"));
}

A and I want to convert them into list of aggregated objects NewView.

NewView class look like this:

public static class NewView {
    private String id;
    private String name;
    private List<String> docId = new ArrayList<>();
}

Expected Output for the sample data provided above would be:

{
  "id": "1234",
  "name": "john",
  "docIds": ["ab123", "cd456", "ef789"]
},
{
  "id": "5678",
  "name": "jack",
  "docIds": ["jh987", "ij654", "kl321"]
},
{
  "id": "9876",
  "name": "ben",
  "docIds": ["mn123", "op456"]
}

I've tried something like this:

Map<String, List<String>> docIdsById = viewList.stream()
    .collect(groupingBy(
        View::getId,
        Collectors.mapping(View::getDocId, Collectors.toList())
    ));

Map<String, List<View>> views = viewList.stream()
    .collect(groupingBy(View::getId));

List<NewView> newViewList = new ArrayList<>();

for (Map.Entry<String, List<View>> stringListEntry : views.entrySet()) {
    View view = stringListEntry.getValue().get(0);
    newViewList.add(new NewView(
            view.getId(),
            view.getName(),
            docIdsById.get(stringListEntry.getKey()))
    );
}

Can I create a list of NewView in only one Stream?


Solution

  • It can be done by in a single stream statement.

    For that we can define a custom Collector via static method Collector.of() which would be used as a downstream of groupingBy() to perform mutable reduction of the View instances having the same id (and consequently mapped to the same key).

    It would also require creating a custom accumulation type that would serve a mean of mutable reduction and eventually would be transformed into a NewView.

    Note that NewView can also serve as the accumulation type, in case if it's mutable (I would make a safe assumption, that it's not and create a separate class for that purpose).

    That's how the stream producing the resulting list might look like:

    List<View> viewList = // initialing the list
        
    List<NewView> newViewList = viewList.stream()
        .collect(Collectors.groupingBy(
            View::getId,
            Collector.of(
                ViewMerger::new,
                ViewMerger::accept,
                ViewMerger::merge,
                ViewMerger::toNewView
            )
        ))
        .values().stream().toList();
    

    That's how such accumulation type might look like. For convenience, I've implemented the contract of Consumer interface:

    public static class ViewMerger implements Consumer<View> {
        private String id;
        private String name;
        private List<String> docIds = new ArrayList<>();
        
        // no args-constructor
    
        @Override
        public void accept(View view) {
            if (id == null) id = view.getId();
            if (name == null) name = view.getName();
            
            docIds.add(view.getDocId());
        }
        
        public ViewMerger merge(ViewMerger other) {
            this.docIds.addAll(other.docIds);
            return this;
        }
        
        public NewView toNewView() {
            return new NewView(id, name, docIds);
        }
    }