Reputation: 33
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
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
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
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