samolet2415
samolet2415

Reputation: 33

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?

Upvotes: 3

Views: 447

Answers (3)

WJS
WJS

Reputation: 40034

You can do it with streams but there would probably be some internal iterations or other streams required or perhaps having to write a custom collector. And I assure you it is not as easy as the following:

Here I am using records to facilitate the demo. They behave like immutable classes with getters auto-generated. I modified the toString() of NewView for display.

record View(String getId, String getDocId, String getName) {
}

record NewView(String getId, String getName, List<String> getDocIds) {
    @Override
    public String toString() {
        return getName + ", " + getId + ", " + getDocIds;
    }
}

The process creates a Map to hold the resulting NewView class with a common key. I chose name, but Id would have also worked. If the existing key is not present, a new NewValue instance is created. That is then returned and the list is retrieved and the associated docId is added to the list.

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

Map<String, NewView> results = new HashMap<>();

for (View view : viewList) {
    results.computeIfAbsent(view.getName(),
        v -> new NewView(view.getId(), view.getName(),
            new ArrayList<>()))
        .getDocIds().add(view.getDocId());
}

The values are stored in a Collection and can be iterated as such to print.

for (NewView v : results.values()) {
    System.out.println(v);
}

prints

ben, 9876, [mn123, op456]
john, 1234, [ab123, cd456, ef789]
jack, 5678, [jh987, ij654, kl321]

For index retrieval you would need to add them to List<NewView>

List<NewView> newViewList = new ArrayList<>(results.values());
System.out.println(newViewList.get(1));

prints

john, 1234, [ab123, cd456, ef789]

Upvotes: 0

iamgirdhar
iamgirdhar

Reputation: 1155

Using groupingBy Multiple fields

You can try the approach of groupingBy using multiple fields.

Here,

I have grouped it by id and name and then iterate over it to prepare a list of NewView Objects as shown below:

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

    viewList.stream()
                .collect(Collectors.groupingBy(View::getId,
                         Collectors.groupingBy(View::getName,
                  Collectors.mapping(View::getDocId,Collectors.toList()))))
                            .forEach((k,v) -> 
                       list.add(new NewView(k, (String) v.keySet().toArray()[0], 
                              (List<String>) v.values().toArray()[0])));

        System.out.println(list);

Output::

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

Upvotes: 1

Alexander Ivanchenko
Alexander Ivanchenko

Reputation: 28988

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

Upvotes: 1

Related Questions