3

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?

0

3 Answers 3

1

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);
    }
}
Sign up to request clarification or add additional context in comments.

Comments

1

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

Comments

0

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]

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.