Aggregate objects into collections in Java using streams - java

I have a list of objects of structure
public class SimpleObject {
private TypeEnum type;
private String propA;
private Integer propB;
private String propC;
}
which I would like to "pack" into the following objects
public class ComplexObject {
private TypeEnum type;
private List<SimpleObject> simpleObjects;
}
based on TypeEnum.
In other words I'd like to create a sort of aggregations which will hold every SimpleObject that contains a certain type. I thought of doing it with Java 8 streams but I don't know how.
So let's say I'm getting the list of SimpleObjects like
#Service
#RequiredArgsConstructor(onConstructor = #__(#Autowired))
public class DataService {
private final DataRepository dataRepo;
public void getPackedData() {
dataRepo.getSimpleObjects().stream()...
}
}
How the next steps should look like? Thank you in advance for any help
I'm using Java 14 with Spring Boot

You can use Collectors.groupingBy to group by type which return Map<TypeEnum, List<SimpleObject>>. Then again stream over map's entryset to convert into List<ComplexObject>
List<ComplexObject> res =
dataRepo.getSimpleObjects()
.stream()
.collect(Collectors.groupingBy(SimpleObject::getType)) //Map<TypeEnum, List<SimpleObject>>
.entrySet()
.stream()
.map(e -> new ComplexObject(e.getKey(), e.getValue())) // ...Stream<ComplexObject>
.collect(Collectors.toList());

You can achieve this with Collectors.groupingBy and further conversion of entries to a list of objects using Collectors.collectingAndThen.
List<SimpleObject> list = ...
List<ComplexObject> map = list.stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(SimpleObject::getType), // group by TypeEnum
map -> map.entrySet() // map entries to ..
.stream()
.map(e -> new ComplexObject(e.getKey(), e.getValue())) // .. ComplexObject
.collect(Collectors.toList()))); // .. to List
I am not currently aware of another solution as long as Stream API is not friendly in processing dictionary structures.

Related

How to group Objects by property using using collector groupingBy() on top of flatMap() in Java 8

How to create Map<String,List<Product>> of below. Here, String (key of the Map) is the category of a Product.
One product can belong to multiple categories, like in the example below.
I am trying with below code, however not able to get next operation:
products.stream()
.flatMap(product -> product.getCategories().stream())
. // how should I progress from here?
Result should be like below:
{electonics=[p1,p3,p4], fashion=[p1,p2,p4], kitchen=[p1,p2,p3],
abc1=[p2], xyz1=[p3],pqr1=[p4]}
Product p1 = new Product(123, Arrays.asList("electonics,fashion,kitchen".split(",")));
Product p2 = new Product(123, Arrays.asList("abc1,fashion,kitchen".split(",")));
Product p3 = new Product(123, Arrays.asList("electonics,xyz1,kitchen".split(",")));
Product p4 = new Product(123, Arrays.asList("electonics,fashion,pqr1".split(",")));
List<Product> products = Arrays.asList(p1, p2, p3, p4);
class Product {
int price;
List<String> categories;
public Product(int price) {
this.price = price;
}
public Product(int price, List<String> categories) {
this.price = price;
this.categories = categories;
}
public int getPrice() {
return price;
}
public List<String> getCategories() {
return categories;
}
}
If you want to use collector groupingBy() for some reason, then you can define a wrapper class (with Java 16+ a record would be more handy for that purpose) which would hold a reference to a category and a product to represent every combination category/product which exist in the given list.
public record ProductCategory(String category, Product product) {}
Pre-Java 16 alternative:
public class ProductCategory {
private String category;
private Product product;
// constructor and getters
}
And then in the make use of the combination of collectors mapping() and toList() as the downstream collector of groupingBy().
List<Product> products = // initializing the list of products
Map<String, List<Product>> productsByCategory = products.stream()
.flatMap(product -> product.getCategories().stream()
.map(category -> new ProductCategory(category, product)))
.collect(Collectors.groupingBy(
ProductCategory::category, // ProductCategory::getCategory if you used a class instead of record
Collectors.mapping(ProductCategory::product, // ProductCategory::getProduct if you used a class instead of record
Collectors.toList())
));
A link to Online-Demo
But instead of creating intermediate objects and generating nested streams, the more performant option would be to describe the accumulation strategy within the three-args version of collect() (or define a custom collector).
That's how it might be implemented:
Map<String, List<Product>> productsByCategory = products.stream()
.collect(
HashMap::new,
(Map<String, List<Product>> map, Product next) -> next.getCategories()
.forEach(category -> map.computeIfAbsent(category, k -> new ArrayList<>())
.add(next)),
(left, right) -> right.forEach((k, v) ->
left.merge(k, v,(oldProd, newProd) -> { oldProd.addAll(newProd); return oldProd; }))
);
A link to Online-Demo
I tried a few things and came up with the following solution:
Map<Object, List<Product>> result =
products.stream()
.flatMap(product -> product.getCategories().stream().map(p -> Map.entry(p, product)))
.collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
System.out.println(result);
Output:
xyz1=[org.example.Product#15db9742], electonics=[org.example.Product#6d06d69c, org.example.Product#15db9742, org.example.Product#7852e922], abc1=[org.ex ...
Edit: I have seen that my solution is pretty similar to the other answer. However, my solution uses a Map.Entry instead of a user-defined object to bring the data into the correct shape.
This can also be done with a combination of flatMapping and toMap:
Map<String, List<Product>> obj = products.stream()
.collect(
Collectors.flatMapping(
product -> product.categories().stream()
.map(category -> Map.entry(category, List.of(product))),
Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> Stream.concat(v1.stream(), v2.stream()).toList()
)
));
What happens here is that first, each Product is converted to a Map.Entry<String, List<Product>>, where the key is the category and the value is the Product itself, or, more precisely, a List<Product>, where this list initially only contains the current product.
Then you could "unpack" the map entries by using toMap. Of course, for those cases where the key (=category) is the same, the values (that is, the List with the Products) must be merged.
Note: I used a Map.Entry here, but you can also write a custom class which is semantically more desirable (something like CategoryProductsMapping(String category, List<Product> products).
I am aware that the owner inquired about groupby and flatmap, but just in case, I'll mention reduce.
I believe this is kind simple; it feels like .collect( method with 3 args that #Alexander Ivanchenko metioned.
in order to use parallelstream, you must merges two hashmaps, I don’t think it’s a good idea, there are extra iteration, I don’t think it’s useful in this case.
HashMap<String, List<Product>> reduce = products.stream().reduce(
new HashMap<>(),
(result, product) -> {
product.getCategories().stream().distinct().forEach(
category -> result.computeIfAbsent(category, k -> new ArrayList<>())
.add(product)
);
return result;
},
(x, y) -> {
throw new RuntimeException("does not support parallel!");
}
);

Collection<Integer> cannot be converted to int while using Stream API

I want to do a Map<Person, Double>, where Double is average of Integer values that is stored in another Map <String, Integer> which is one of the fields of stream's elements.
public Map<Person,Double> totalScores(Stream<CourseResult> programmingResults) {
return
programmingResults.collect(Collectors.groupingBy(
CourseResult::getPerson,
// And there is a problem, I want to get values from `Map <String, Integer>`
// and do the `averagingInt`, but only get
//`Bad return type in lambda expression:
// Collection<Integer> cannot be converted to int`
Collectors.averagingInt(
s -> s.getTaskResults().values()
)
));
}
How can I get these values in the right way?
There's some of the classes I'm using:
public class CourseResult {
private final Person person;
private final Map<String, Integer> taskResults;
public CourseResult(final Person person, final Map<String, Integer> taskResults) {
this.person = person;
this.taskResults = taskResults;
}
public Person getPerson() {
return person;
}
public Map<String, Integer> getTaskResults() {
return taskResults;
}
}
Note that values() is a Collection<Integer>. You can't average by that. You can use a flat-mapping Collector, to flatten each group of persons to just integers, rather than CourseResult. After that, you can do an average by the identity function.
return programmingResults.collect(
Collectors.groupingBy(
CourseResult::getPerson,
Collectors.flatMapping(s->s.getTaskResults().values().stream(),
Collectors.averagingInt(x -> x)
)
)
);
Edit: If there is only one CourseResult per Person, then you don't need groupingBy. Just use toMap and calculate the average using another stream.
return programmingResults.collect(
Collectors.toMap(
CourseResult::getPerson,
result -> result.getTaskResults().values()
.stream().mapToInt(x -> x).average().orElse(0))
);
If it is guaranteed that the input stream contains CourseResult instances which have unique persons (and grouping + flatMapping of the task results may not be needed), it may be sufficient to use toMap collector:
public Map<Person,Double> totalScores(Stream<CourseResult> results) {
return
results.collect(Collectors.toMap(
CourseResult::getPerson,
cr -> cr.getTaskResults().values() // Collection<Integer>
.stream() // Stream<Integer>
.collect(Collectors.averagingInt(Integer::intValue))
)
));
}

Project reactor, Convert Flux into Map<String,List<Data>>

I have a list of Data object
public class Data{
private String id;
private String sharedId;
}
How to convert this list into Map<String,List<Data>>, collecting the data object sharing the same sharedId together
I have tried to use this line of code, but no luck
Map<String,List<Data>> dataMap= Flux.fromIterable(list)
.collectMap(item-> item.getSharedId(),
item-> item);
Stream API is not equal to Reactive API and they are not interchangeable.
Use Stream API for grouping into Map<String, List<Data>> from List<Data>:
Map<String,List<Data>> dataMap = list.stream()
.collect(Collectors.groupingBy(Data::getSharedId));
Using Reactive API to achieve you achieve Flux<GroupedFlux<String, Data>> analogically or expand the structure using Map.Entry:
Flux<GroupedFlux<String, Data>> groupedFlux = Flux.fromIterable(list)
.groupBy(Data::getSharedId);
Flux<Map.Entry<String, List<Data>>> groupedFlux = Flux.fromIterable(list)
.groupBy(Data::getSharedId)
.flatMap(group -> group
.collectList()
.map(listOfData -> Map.entry(group.key(), listOfData)));
The following should work:
Map<String,List<Data>> dataMap =
list.stream().collect(groupingBy(Data::getSharedId));
With regards to previous answers from Nikolas & Joao you could also use:
Mono<Map<String, Collection<Data>>> mapCollection = Flux.fromIterable(list)
.collectMultimap(Data::getSharedId, item -> item);
you can map further to specific collection if it needs to be a list:
Mono<Map<String, List<Data>>> mapList = mapCollection
.map(map -> map
.entrySet()
.stream()
.collect(Collectors.toMap(Entry::getKey, e -> List.copyOf(e.getValue()))))
or use:
Mono<Map<String, List<Data>>> mapList2 = Mono.just(list)
.map(l -> l.stream().collect(Collectors.groupingBy(Data::getSharedId)));

Java 8 convert a List<Object> to Map<K,V> using the fields of the object's children?

The situation is simple: I have a list of object List<ParentClass> list
and would like to convert it to Map<orderKey, price>
The class looks like the following
class ParentClass{
Child1Class a;
Child2Class b;
}
The orderKey class looks like this
class orderKey{
String id;
String itemName;
}
The children classes look like this
class Child1Class{
String id;
String itemName;
Date date;
.....
}
class Child2Class{
BigDecimal price;
....
}
All of the classes have corresponded getters and setters for each field. So I essentially want to map the fields of the children's fields. How can I do this?
-----------------------------MY ATTEMPT IS SOMETHING LIKE THIS--------------------------------------
list.stream().
.collect(Collectors
.toMap(ParentClass::getA, ParentClass::getB)
.entrySet().stream()
......
then I'm stuck
Not sure how I can construct the temp object orderKey as the key for the new map. Any help would be appreciated.
Try it like this. Since you didn't provide getters I had to access the fields. It also presumes an OrderKey constructor.
Map<OrderKey,BigDecimal> map = list
.stream()
.collect(Collectors
.toMap(parent ->
new OrderKey(parent.a.id,
parent.a.itemName),
parent->parent.b.price));
With getters, it would look like this.
Map<OrderKey,BigDecimal> map=
list.stream()
.collect(Collectors
.toMap(parent ->
new OrderKey(parent.getA().getId(),
parent.getA().getItemName()),
parent->parent.getB().getPrice()));
Map<K, V> map = list.stream().
.collect(Collectors.toMap(
parent -> {
// the key in the map is of type orderKey, instantiate it here, you have "parent" which is one element of the list.
return new orderKey(...);
},
ParentClass::getB
));

How to map to multiple elements with Java 8 streams?

I have a class like this:
class MultiDataPoint {
private DateTime timestamp;
private Map<String, Number> keyToData;
}
and i want to produce , for each MultiDataPoint
class DataSet {
public String key;
List<DataPoint> dataPoints;
}
class DataPoint{
DateTime timeStamp;
Number data;
}
of course a 'key' can be the same across multiple MultiDataPoints.
So given a List<MultiDataPoint>, how do I use Java 8 streams to convert to List<DataSet>?
This is how I am currently doing the conversion without streams:
Collection<DataSet> convertMultiDataPointToDataSet(List<MultiDataPoint> multiDataPoints)
{
Map<String, DataSet> setMap = new HashMap<>();
multiDataPoints.forEach(pt -> {
Map<String, Number> data = pt.getData();
data.entrySet().forEach(e -> {
String seriesKey = e.getKey();
DataSet dataSet = setMap.get(seriesKey);
if (dataSet == null)
{
dataSet = new DataSet(seriesKey);
setMap.put(seriesKey, dataSet);
}
dataSet.dataPoints.add(new DataPoint(pt.getTimestamp(), e.getValue()));
});
});
return setMap.values();
}
It's an interesting question, because it shows that there are a lot of different approaches to achieve the same result. Below I show three different implementations.
Default methods in Collection Framework: Java 8 added some methods to the collections classes, that are not directly related to the Stream API. Using these methods, you can significantly simplify the implementation of the non-stream implementation:
Collection<DataSet> convert(List<MultiDataPoint> multiDataPoints) {
Map<String, DataSet> result = new HashMap<>();
multiDataPoints.forEach(pt ->
pt.keyToData.forEach((key, value) ->
result.computeIfAbsent(
key, k -> new DataSet(k, new ArrayList<>()))
.dataPoints.add(new DataPoint(pt.timestamp, value))));
return result.values();
}
Stream API with flatten and intermediate data structure: The following implementation is almost identical to the solution provided by Stuart Marks. In contrast to his solution, the following implementation uses an anonymous inner class as intermediate data structure.
Collection<DataSet> convert(List<MultiDataPoint> multiDataPoints) {
return multiDataPoints.stream()
.flatMap(mdp -> mdp.keyToData.entrySet().stream().map(e ->
new Object() {
String key = e.getKey();
DataPoint dataPoint = new DataPoint(mdp.timestamp, e.getValue());
}))
.collect(
collectingAndThen(
groupingBy(t -> t.key, mapping(t -> t.dataPoint, toList())),
m -> m.entrySet().stream().map(e -> new DataSet(e.getKey(), e.getValue())).collect(toList())));
}
Stream API with map merging: Instead of flattening the original data structures, you can also create a Map for each MultiDataPoint, and then merge all maps into a single map with a reduce operation. The code is a bit simpler than the above solution:
Collection<DataSet> convert(List<MultiDataPoint> multiDataPoints) {
return multiDataPoints.stream()
.map(mdp -> mdp.keyToData.entrySet().stream()
.collect(toMap(e -> e.getKey(), e -> asList(new DataPoint(mdp.timestamp, e.getValue())))))
.reduce(new HashMap<>(), mapMerger())
.entrySet().stream()
.map(e -> new DataSet(e.getKey(), e.getValue()))
.collect(toList());
}
You can find an implementation of the map merger within the Collectors class. Unfortunately, it is a bit tricky to access it from the outside. Following is an alternative implementation of the map merger:
<K, V> BinaryOperator<Map<K, List<V>>> mapMerger() {
return (lhs, rhs) -> {
Map<K, List<V>> result = new HashMap<>();
lhs.forEach((key, value) -> result.computeIfAbsent(key, k -> new ArrayList<>()).addAll(value));
rhs.forEach((key, value) -> result.computeIfAbsent(key, k -> new ArrayList<>()).addAll(value));
return result;
};
}
To do this, I had to come up with an intermediate data structure:
class KeyDataPoint {
String key;
DateTime timestamp;
Number data;
// obvious constructor and getters
}
With this in place, the approach is to "flatten" each MultiDataPoint into a list of (timestamp, key, data) triples and stream together all such triples from the list of MultiDataPoint.
Then, we apply a groupingBy operation on the string key in order to gather the data for each key together. Note that a simple groupingBy would result in a map from each string key to a list of the corresponding KeyDataPoint triples. We don't want the triples; we want DataPoint instances, which are (timestamp, data) pairs. To do this we apply a "downstream" collector of the groupingBy which is a mapping operation that constructs a new DataPoint by getting the right values from the KeyDataPoint triple. The downstream collector of the mapping operation is simply toList which collects the DataPoint objects of the same group into a list.
Now we have a Map<String, List<DataPoint>> and we want to convert it to a collection of DataSet objects. We simply stream out the map entries and construct DataSet objects, collect them into a list, and return it.
The code ends up looking like this:
Collection<DataSet> convertMultiDataPointToDataSet(List<MultiDataPoint> multiDataPoints) {
return multiDataPoints.stream()
.flatMap(mdp -> mdp.getData().entrySet().stream()
.map(e -> new KeyDataPoint(e.getKey(), mdp.getTimestamp(), e.getValue())))
.collect(groupingBy(KeyDataPoint::getKey,
mapping(kdp -> new DataPoint(kdp.getTimestamp(), kdp.getData()), toList())))
.entrySet().stream()
.map(e -> new DataSet(e.getKey(), e.getValue()))
.collect(toList());
}
I took some liberties with constructors and getters, but I think they should be obvious.

Categories