I am working on a framework where we are trying to convert our traditional loops to streams. My problem is I wrote two separate logics to get price and colors but I would like to merge both together so it will be presentable
Code to get the price values
List<Double> productPrices = product.getUpcs()
.stream()
.map(e -> e.getUpcDetails().getPrice().getRetail().getPriceValue())
.distinct()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
Code to get the colors under prices
product.getUpcs()
.stream()
.filter(e -> e.getUpcDetails().getPrice().getRetail().getPriceValue() == 74.5)
.flatMap(e -> e.getUpcDetails().getAttributes().stream())
.filter(e2 -> e2.getName().contentEquals("COLOR"))
.forEach(e3 -> System.out.println(e3.getValues().get(0).get("value")));
I harcoded price in the above section to obtain the colors, instead, i would like to get that as input from the list of price values and get an output in
Map<Double,List<colors>
output Map<75.4, {blue,black,orange}>
I tried merging these both without success, any help would be appriciated.
I would suggest you examine this or similar tutorial to get a bit of understanding how this works.
The key to the solution is to learn about Collectors.groupingBy() functionality. As a side note, there it also shows a better way of handling pricing information in Java.
But what you would need to do is something like this:
Map<Double, Set<String>> productPrices = product
.stream()
.map(e -> e.getUpcDetails())
.collect(
Collectors.groupingBy(Details::getPrice,
Collectors.mapping(Details::getColors, Collectors.collectingAndThen(
Collectors.toList(),
(set) -> set
.stream()
.flatMap(Collection::stream)
.collect(Collectors.toSet())))
));
Since your question is a bit unclear about the details of classes involved, I assumed this simple class structure:
class Details {
private double price;
private List<String> colors;
double getPrice() { return price; }
List<String> getColors() { return colors; }
}
class Product {
private Details details;
Details getUpcDetails() { return details; }
}
```
It would be possible to optimize the code above but I specifically left the possibility to filter and map colours in the mapping collector.
You can first turn your second stream into a method that gets a List of products (assumed to be filtered/grouped by price) and transforms it to a List of colors:
List<Color> productsToColors(final List<Product> products) {
return products.stream()
.flatMap(e -> e.getUpcDetails().getAttributes().stream())
.filter(e2 -> e2.getName().contentEquals("COLOR"))
.map(e3 -> e3.getValues().get(0).get("value"))
.collect(toList());
}
You can use the groupingBy collector to gather all products by their price in a List and then with a second create a second stream and the productsToColors method get the map you want:
Map<Double, List<Color>> colors = product.getUpcs().stream()
.collect(groupingBy(e -> e.getUpcDetails().getPrice().getRetail().getPriceValue())
.entrySet().stream()
.collect(toMap(Entry::getKey, e -> productsToColors(e.getValue())));
You can also have groupingBy create a TreeMap instead so that the colors map will be sorted by price.
As a side-note beware of comparing double values for equality like this. You may want to round them first. Or use long variables multiplied by 100 (i.e. cents).
Related
I have a input object
#Getter
class Txn {
private String hash;
private String withdrawId;
private String depositId;
private Integer amount;
private String date;
}
and the output object is
#Builder
#Getter
class UserTxn {
private String hash;
private String walletId;
private String txnType;
private Integer amount;
}
In the Txn object transfers the amount from the withdrawId -> depositId.
what I am doing is I am adding all the transactions (Txn objects) in a single amount grouped by hash.
but for that I have to make two streams for groupingby withdrawId and second or for depositId and then the third stream for merging them
grouping by withdrawId
var withdrawStream = txnList.stream().collect(Collectors.groupingBy(Txn::getHash, LinkedHashMap::new,
Collectors.groupingBy(Txn::getWithdrawId, LinkedHashMap::new, Collectors.toList())))
.entrySet().stream().flatMap(hashEntrySet -> hashEntrySet.getValue().entrySet().stream()
.map(withdrawEntrySet ->
UserTxn.builder()
.hash(hashEntrySet.getKey())
.walletId(withdrawEntrySet.getKey())
.txnType("WITHDRAW")
.amount(withdrawEntrySet.getValue().stream().map(Txn::getAmount).reduce(0, Integer::sum))
.build()
));
grouping by depositId
var depositStream = txnList.stream().collect(Collectors.groupingBy(Txn::getHash, LinkedHashMap::new,
Collectors.groupingBy(Txn::getDepositId, LinkedHashMap::new, Collectors.toList())))
.entrySet().stream().flatMap(hashEntrySet -> hashEntrySet.getValue().entrySet().stream()
.map(withdrawEntrySet ->
UserTxn.builder()
.hash(hashEntrySet.getKey())
.walletId(withdrawEntrySet.getKey())
.txnType("DEPOSIT")
.amount(withdrawEntrySet.getValue().stream().map(Txn::getAmount).reduce(0, Integer::sum))
.build()
));
then merging them again, using deposites - withdraws
var res = Stream.concat(withdrawStream, depositStream).collect(Collectors.groupingBy(UserTxn::getHash, LinkedHashMap::new,
Collectors.groupingBy(UserTxn::getWalletId, LinkedHashMap::new, Collectors.toList())))
.entrySet().stream().flatMap(hashEntrySet -> hashEntrySet.getValue().entrySet().stream()
.map(withdrawEntrySet -> {
var depositAmount = withdrawEntrySet.getValue().stream().filter(userTxn -> userTxn.txnType.equals("DEPOSIT")).map(UserTxn::getAmount).reduce(0, Integer::sum);
var withdrawAmount = withdrawEntrySet.getValue().stream().filter(userTxn -> userTxn.txnType.equals("WITHDRAW")).map(UserTxn::getAmount).reduce(0, Integer::sum);
var totalAmount = depositAmount-withdrawAmount;
return UserTxn.builder()
.hash(hashEntrySet.getKey())
.walletId(withdrawEntrySet.getKey())
.txnType(totalAmount > 0 ? "DEPOSIT": "WITHDRAW")
.amount(totalAmount)
.build();
}
));
My question is, How can I do this in one stream.
Like by somehow groupingBy withdrawId and depositId is one grouping.
something like
res = txnList.stream()
.collect(Collectors.groupingBy(Txn::getHash,
LinkedHashMap::new,
Collectors.groupingBy(Txn::getWithdrawId && Txn::getDepositId,
LinkedHashMap::new, Collectors.toList())))
.entrySet().stream().flatMap(hashEntrySet -> hashEntrySet.getValue().entrySet().stream()
.map(walletEntrySet ->
{
var totalAmount = walletEntrySet.getValue().stream().map(
txn -> Objects.equals(txn.getDepositId(), walletEntrySet.getKey())
? txn.getAmount() : (-txn.getAmount())).reduce(0, Integer::sum);
return UserTxn.builder()
.hash(hashEntrySet.getKey())
.walletId(walletEntrySet.getKey())
.txnType("WITHDRAW")
.amount(totalAmount)
.build();
}
));
TL;DR
For those who didn't understand the question, OP wants to generate from each Txn instance (Txn probably stands for transaction) two peaces of data: hash and withdrawId + aggregated amount, and hash and depositId + aggregated amount.
And then they want to merge the two parts together (for that reason they were creating the two streams, and then concatenating them).
Note: it seems like there's a logical flow in the original code: the same amount gets associated with withdrawId and depositId. Which doesn't reflect that this amount has been taken from one account and transferred to another. Hence, it would make sense if for depositId amount would be used as is, and for withdrawId - negated (i.e. -1 * amount).
Collectors.teeing()
You can make use of the Java 12 Collector teeing() and internally group stream elements into two distinct Maps:
the first one by grouping the stream data by withdrawId and hash.
and another one by grouping the data depositId and hash.
Teeing expects three arguments: 2 downstream Collectors and a Function combining the results produced by collectors.
As the downstream of teeing() we can use a combination of Collectors groupingBy() and summingInt(), the second one is needed to accumulate integer amount of the transaction.
Note that there's no need in using nested Collector groupingBy() instead we can create a custom type that would hold id and hash (and its equals/hashCode should be implemented based on the wrapped id and hash). Java 16 record fits into this role perfectly well:
public record HashWalletId(String hash, String walletId) {}
Instances of HashWalletId would be used as Keys in both intermediate Maps.
The finisher function of teeing() would merge the results of the two Maps together.
The only thing left is to generate instances of UserTxn out of map entries.
List<Txn> txnList = // initializing the list
List<UserTxn> result = txnList.stream()
.collect(Collectors.teeing(
Collectors.groupingBy(
txn -> new HashWalletId(txn.getHash(), txn.getWithdrawId()),
Collectors.summingInt(txn -> -1 * txn.getAmount())), // because amount has been withdrawn
Collectors.groupingBy(
txn -> new HashWalletId(txn.getHash(), txn.getDepositId()),
Collectors.summingInt(Txn::getAmount)),
(map1, map2) -> {
map2.forEach((k, v) -> map1.merge(k, v, Integer::sum));
return map1;
}
))
.entrySet().stream()
.map(entry -> UserTxn.builder()
.hash(entry.getKey().hash())
.walletId(entry.getKey().walletId())
.txnType(entry.getValue() > 0 ? "DEPOSIT" : "WITHDRAW")
.amount(entry.getValue())
.build()
)
.toList(); // remove the terminal operation if your goal is to produce a Stream
I wouldn’t use this in my code because I think it’s not readable and will be very hard to change and manage in the future(SOLID).
But in case you still want this-
If I got your design right hash is unique per user and transaction will only have deposit or withdrawal, if so, this will work-
You could triple groupBy via collectors chaining like you did in your example.
You can create the Txn type via simple map function just check which id is null.
Map<String, Map<String, Map<String, List<Txn>>>> groupBy =
txnList.stream()
.collect(Collectors.groupingBy(Txn::getHash, LinkedHashMap::new,
Collectors.groupingBy(Txn::getDepositId, LinkedHashMap::new,
Collectors.groupingBy(Txn::getWithdrawId, LinkedHashMap::new, Collectors.toList()))));
then use the logic from your example on this stream.
This question is about Java Streams' groupingBy capability.
Suppose I have a class, WorldCup:
public class WorldCup {
int year;
Country champion;
// all-arg constructor, getter/setters, etc
}
and an enum, Country:
public enum Country {
Brazil, France, USA
}
and the following code snippet:
WorldCup wc94 = new WorldCup(1994, Country.Brazil);
WorldCup wc98 = new WorldCup(1998, Country.France);
List<WorldCup> wcList = new ArrayList<WorldCup>();
wcList.add(wc94);
wcList.add(wc98);
Map<Country, List<Integer>> championsMap = wcList.stream()
.collect(Collectors.groupingBy(WorldCup::getCountry, Collectors.mapping(WorldCup::getYear));
After running this code, championsMap will contain:
Brazil: [1994]
France: [1998]
Is there a succinct way to have this list include an entry for all of the values of the enum? What I'm looking for is:
Brazil: [1994]
France: [1998]
USA: []
There are several approaches you can take.
The map which would be used for accumulating the stream data can be prepopulated with entries corresponding to every enum-member. To access all existing enum-members you can use values() method or EnumSet.allOf().
It can be achieved using three-args version of collect() or through a custom collector created via Collector.of().
Map<Country, List<Integer>> championsMap = wcList.stream()
.collect(
() -> EnumSet.allOf(Country.class).stream() // supplier
.collect(Collectors.toMap(
Function.identity(),
c -> new ArrayList<>()
)),
(Map<Country, List<Integer>> map, WorldCup next) -> // accumulator
map.get(next.getCountry()).add(next.getYear()),
(left, right) -> // combiner
right.forEach((k, v) -> left.get(k).addAll(v))
);
Another option is to add missing entries to the map after reduction of the stream has been finished.
For that we can use built-in collector collectingAndThen().
Map<Country, List<Integer>> championsMap = wcList.stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(WorldCup::getCountry,
Collectors.mapping(WorldCup::getYear,
Collectors.toList())),
map -> {
EnumSet.allOf(Country.class)
.forEach(country -> map.computeIfAbsent(country, k -> new ArrayList<>())); // if you're not going to mutate these lists - use Collections.emptyList()
return map;
}
));
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!");
}
);
This question already has an answer here:
Collect complex objects in one lambda expression
(1 answer)
Closed 3 years ago.
I have below class objects in one ArrayList:
new Pojo("Football","M",1000)
new Pojo("Football","M",1000)
new Pojo("Cricket","F",500)
new Pojo("Cricket","F",1500)
new Pojo("Cricket","M",500)
new Pojo("Cricket","M",500)
I want to get the average points of the "Sports" with respect to "Gender".
I came across Java 8 Streams and seems like it is good to use it here but I don't know to implement it.
I have tried it like below:
Map<Object,Long> mp = pojoList.stream().collect(Collectors.groupingBy(p -> p.sport,Collectors.counting()));
It's working good, giving me a sports count but I want to apply one more filter of gender and get the average point.
The output which I want:
new PojoOutput("Football","M",1000)
new PojoOutput("Cricket","F",1000)
new PojoOutput("Cricket","M",500)
You can group to a map by using 2 groupingBy calls, one of them being the downstream to the other:
Map<String, Map<String, Double>> res = pojos.stream()
.collect(Collectors.groupingBy(Pojo::getSport,
Collectors.groupingBy(Pojo::getGender,
Collectors.averagingDouble(i -> i.getPoints()))));
This results in {Cricket={F=1000.0, M=500.0}, Football={M=1000.0}}, which you can easily convert to Pojo instances using something like this:
List<Pojo> groups = result.entrySet()
.stream()
.flatMap(sport -> sport.getValue()
.entrySet().stream()
.map(gender -> new Pojo(sport.getKey(),
gender.getKey(),
gender.getValue())))
.collect(Collectors.toList());
Result with a generated toString:
[Pojo [sport=Cricket, gender=F, points=1000.0],
Pojo [sport=Cricket, gender=M, points=500.0],
Pojo [sport=Football, gender=M, points=1000.0]]
You can use a SimpleEntry to group your data with. After that you can map the results back to your PojoOutput:
List<PojoOutput> mp = pojoList.stream()
.collect(Collectors.groupingBy(x -> new AbstractMap.SimpleEntry<>(x.getSport(), x.getGender()), Collectors.averagingInt(Pojo::getScore)))
.entrySet().stream()
.map(e -> new PojoOutput(e.getKey().getKey(), e.getKey().getValue(), e.getValue()))
.collect(Collectors.toList());
You can improve that solution a bit using your PojoOutput directly in the group by clause. Therefore the PojoOutput needs a constructor with Pojo as attribute and implement equals() and hashCode() according to the fields: For example something similar to that:
private static class PojoOutput {
private String sport;
private String gender;
private double average;
public PojoOutput(Pojo pojo) {
this.sport = pojo.getSport();
this.gender = pojo.getGender();
}
public void setAverage(double average) {
this.average = average;
}
// equals, hashCode and getter
}
Now you can use that class like that:
List<PojoOutput> mp = pojoList.stream()
.collect(Collectors.groupingBy(PojoOutput::new, Collectors.averagingInt(Pojo::getScore)))
.entrySet().stream()
.peek(p -> p.getKey().setAverage(p.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
The result in both cases is:
PojoOutput[sport='Cricket', gender='M', average=500.0]
PojoOutput[sport='Football', gender='M', average=1000.0]
PojoOutput[sport='Cricket', gender='F', average=1000.0]
You could use Arrays::asList for the Key that you want to group by:
pojoList.stream()
.collect(Collectors.groupingBy(
x -> Arrays.asList(x.getSport(), x.getGender()),
Collectors.averagingInt(Pojo::getScore)));
Or if you have a PojoOutput, simply replace the x -> Arrays.asList(x.getSport(), x.getGender()) with that constructor.
You want the key of the map to be [sport, gender]. So you need to create a class SportAndGenderKey containing these two properties and a proper equals and hashCode methods, and then you need to group your objects by SportAndGenderKey.
And then you want to use an averaging collector rather than a counting collector.
Note: the key could simply be a List containing the two properties, but I find it less clear than a dedicated key class.
As #JB Nizet said you can follow bellow code:
list.stream()
.collect(Collectors.toMap(p ->
new PojoOutput(p.getSport(), p.getGender()),Pojo::getPoint, (v1, v2) -> (v1 + v2) / 2))
.entrySet()
.stream()
.map(entry -> entry.getKey().setAverage(entry.getValue()))
.collect(Collectors.toList())
just override equals and hashCode methods in PojoOutput class based on two properties (sport & geneder)
and also:
public PojoOutput setAverage(Integer average) {
this.average = average;
return this;
}
I have two lists of objects:
List<SampleClassOne> listOne;
List<SampleClassTwo> listTwo;
SampleClassOne:
public class SampleClassOne{
private String myFirstProperty;
//ommiting getters-setters
}
SampleClassTwo:
public class SampleClassTwo{
private String myOtherProperty;
//ommiting getters-setters
}
RootSampleClass:
public class RootSampleClass{
private SampleClassOne classOne;
private SampleClassTwo classTwo;
//ommiting getters-setters
}
Now I would like to merge two lists into new list of type RootSampleClass based on condition:
if(classOneObject.getMyFirstProperty().equals(classTwoObject.getMyOtherProperty()){
//create new RootSampleClass based on classOneObject and classTwoObject and add it to another collection
}
Pseudo code:
foreach(one: collectionOne){
foreach(two: collectionTwo){
if(one.getMyFirstProperty().equals(two.getMyOtherProperty()){
collectionThree.add(new RootSampleClass(one, two));
}
}
}
I am interested in java 8. I would like to have the best performance here that's why I am asking for existing solution without writing custom foreach.
A direct equivalent to the nested loops is
List<RootSampleClass> result = listOne.stream()
.flatMap(one -> listTwo.stream()
.filter(two -> one.getMyFirstProperty().equals(two.getMyOtherProperty()))
.map(two -> new RootSampleClass(one, two)))
.collect(Collectors.toList());
with an emphasis on direct equivalent, which includes the bad performance of doing n×m operations.
A better solution is to convert one of the lists into a data structure supporting an efficient lookup, e.g. a hash map. This consideration is independent of the question which API you use. Since you asked for the Stream API, you can do it like this:
Map<String,List<SampleClassOne>> tmp=listOne.stream()
.collect(Collectors.groupingBy(SampleClassOne::getMyFirstProperty));
List<RootSampleClass> result = listTwo.stream()
.flatMap(two -> tmp.getOrDefault(two.getMyOtherProperty(), Collections.emptyList())
.stream().map(one -> new RootSampleClass(one, two)))
.collect(Collectors.toList());
Note that both solutions will create all possible pairings in case, a property value occurs multiple times within either or both lists. If the property values are unique within each list, like IDs, you can use the following solution:
Map<String, SampleClassOne> tmp=listOne.stream()
.collect(Collectors.toMap(SampleClassOne::getMyFirstProperty, Function.identity()));
List<RootSampleClass> result = listTwo.stream()
.flatMap(two -> Optional.ofNullable(tmp.get(two.getMyOtherProperty()))
.map(one -> Stream.of(new RootSampleClass(one, two))).orElse(null))
.collect(Collectors.toList());
If you don’t mind potentially performing double lookups, you could replace the last solution with the following more readable code:
Map<String, SampleClassOne> tmp=listOne.stream()
.collect(Collectors.toMap(SampleClassOne::getMyFirstProperty, Function.identity()));
List<RootSampleClass> result = listTwo.stream()
.filter(two -> tmp.containsKey(two.getMyOtherProperty()))
.map(two -> new RootSampleClass(tmp.get(two.getMyOtherProperty()), two))
.collect(Collectors.toList());