Is there any way to create this hashmap inside the lambda function?
Map<SaleStatus, Long> sales = new HashMap<>();
saleStatusCounters.forEach(saleStatusCounter -> sales.put(saleStatusCounter.getStatus(), saleStatusCounter.getMatches()));
Something like this:
saleStatusCounters.stream()
.map(obj -> new HashMap<SaleStatus, Long>().put(obj.getStatus(), obj.getMatches()))
.collect(Collectors.toMap(???)));
Your code is fine as is. You can, nonetheless, use streams and Collectors.toMap to get the result you want:
Map<SaleStatus, Long> sales = saleStatusCounters.stream()
.collect(Collectors.toMap(obj -> obj.getStatus(), obj -> obj.getMatches()));
Note: this works as long as there are no collisions in the map, i.e. as long as you don't have two or more sale status counter objects with the same status.
In case you have more than one element in your list with the same status, you should use the overloaded version of Collectors.toMap that expects a merge function:
Map<SaleStatus, Long> sales = saleStatusCounters.stream()
.collect(Collectors.toMap(
obj -> obj.getStatus(),
obj -> obj.getMatches(),
Long::sum));
Here Long::sum is a BinaryOperator<Long> that merges two values that are mapped to the same key.
Related
Given a class Object1
class Object1 {
private Object2 object2;
private List<Object3> object3s;
}
and a List<Object1>
cuurent implementation
Map<Object3, Object2> accountMap = new HashMap<>();
for (Object1 dto : accounts) {
dto.getObject3s()
.forEach(dto -> accountMap.put(dto, dto.getObject2()));
}
How do I create a Map<Object3, Object2> preferably in a stream?
I believe doing this in a stream would look something like this:
Map<Object3, Object2> map = list.stream()
.flatMap(o1 -> o1.object3s.stream().map(o3 -> Map.entry(o3, o1.object2)))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue));
That is, flatmap your stream of Object1 to a stream of entries of Object3 and Object2; and then collect them to a map.
To achieve this with streams, you need an auxiliary object, that would hold references to Object3 and Object2.
A good practice is to use a Java 16 record for that purpose, if you're using an earlier JDK version you can define a class. Sure a quick and dirty option Map.Entry will do as well, but it reduces the readability of code because methods getKey() and getValue() give no clue what they are returning, it would be even confusing if the source of the stream is entry set and there would be several kinds of entries in the pipeline.
Here's how such a record might look like:
public record Objects3And2(Object3 object3, Object2 object2) {}
In the stream, you need to flatten the data by creating a new instance of record Objects3And2 for every nested Object3. For that, we can use either flatMap() of mapMulti().
And to accumulate the data into a map we can make use of the collector `toMap()
Note that in this implementation (as well as in your code) values of duplicated Object3 would be overridden.
List<Object1> accounts = // initializing the list
Map<Object3, Object2> accountMap = accounts.stream()
.flatMap(o1 -> o1.getObject3s().stream()
.map(o3 -> new Objects3And2(o3, o1.getObject2()))
)
.collect(Collectors.toMap(
Objects3And2::object3, // generating a Key
Objects3And2::object2, // generating a Value
(l, r) -> r // resolving Duplicates
));
If you don't want to lose the data, you need a different structure of the resulting map and different collector, namely groupingBy() in conjunction with mapping() as its downstream.
List<Object1> accounts = // initializing the list
Map<Object3, List<Object2>> accountMap = accounts.stream()
.flatMap(o1 -> o1.getObject3s().stream()
.map(o3 -> new Objects3And2(o3, o1.getObject2()))
)
.collect(Collectors.groupingBy(
Objects3And2::object3,
Collectors.mapping(Objects3And2::object2,
Collectors.toList())
));
I have a stream of orders (the source being a list of orders).
Each order has a Customer, and a list of OrderLine.
What I'm trying to achieve is to have a map with the customer as the key, and all order lines belonging to that customer, in a simple list, as value.
What I managed right now returns me a Map<Customer>, List<Set<OrderLine>>>, by doing the following:
orders
.collect(
Collectors.groupingBy(
Order::getCustomer,
Collectors.mapping(Order::getOrderLines, Collectors.toList())
)
);
I'm either looking to get a Map<Customer, List<OrderLine>>directly from the orders stream, or by somehow flattening the list from a stream of the Map<Customer>, List<Set<OrderLine>>> that I got above.
You can simply use Collectors.toMap.
Something like
orders
.stream()
.collect(Collectors
.toMap(Order::getCustomer
, Order::getOrderLines
, (v1, v2) -> { List<OrderLine> temp = new ArrayList<>(v1);
temp.addAll(v2);
return temp;});
The third argument to the toMap function is the merge function. If you don't explicitly provide that and it there is a duplicate key then it will throw the error while finishing the operation.
Another option would be to use a simple forEach call:
Map<Customer, List<OrderLine>> map = new HashMap<>();
orders.forEach(
o -> map.computeIfAbsent(
o.getCustomer(),
c -> new ArrayList<OrderLine>()
).addAll(o.getOrderLines())
);
You can then continue to use streams on the result with map.entrySet().stream().
For a groupingBy approach, try Flat-Mapping Collector for property of a Class using groupingBy
I have a database object that has a field that contains a list of strings. I retrieve all these objects and then use the flatMap and distinct stream methods on the resulting list to get a new list that holds all possible unique values that a database object string list can contain.
Next i want to make a map where the keys are the unique values list of the stringlist that i made earlier, and the values of the map are a list of database objects whose stringlist contains the value of the respective string mapkey.
So what I want is groupingBy the following:
if(object.stringList().contains(respectiveMapKeyFromUniqeStringCollection) put object in object values list of that respective keymap.
Is something like this possible using the groupingBy method?
Edit: I will explain further
class VegetableMaker{
#ElementCollection
private List<String> vegetableList;
}
Lets assume the possible values that a vegetableList can contain are: "Lettuce, Tomato, spinache, rubarbe, onion"
Set<String> produceNames = vegetableMakers.stream().flatMap(vegetableMaker -> vegetableMaker.getVegetableList().stream())
.distinct().collect(Collectors.toSet());
Now we have the list that contains all the possible values mentioned before.
We want to use the values in this list as the keys in the map.
So the Map will look like:
Map<uniqueStringsAsKeys, List<VegetableMaker>> map
The list value contains all the VegetableMaker instances of which the vegetableList contains the key of the map. So the list of key Onion will contain all the VegetableMaker instances whose list includes "Onion".
Is it possible to achieve such a map using the groupingBy method of a java stream?
EDIT 2:
This is the solution i have now, that doesn't use groupingBy but clarifies even more what I want.
EDIT: changed variable in code to match variables used in previous examples.
Set<VegetableMaker> vegetableMakers = vegetableMakerDao.findAll();
Set<String> uniqueVegetableList = vegetableMakers.stream().flatMap(vegetableMaker -> affiliateLink.getKeywords().stream()).distinct().collect(Collectors.toSet());
Map<String,Set<VegetableMaker>> vegetableMakersContainingKeywordInTheirList = new HashMap<>();
uniqueVegetableList.forEach(produceName ->{
Set<VegetableMaker> vegetableMakerSet = new HashSet<>();
vegetableMakers.forEach( vegetableMaker -> {
if(vegetableMaker.getVegetableList().contains(produceName))
vegetableMakerSet.add(vegetableMaker);
});
vegetableMakersContainingKeywordInTheirList.put(produceName, vegetableMakerSet);
});
If I understood you correctly:
List<VegetableMaker> dbObjects = List.of(
new VegetableMaker("Salad", List.of("Onion", "Cucumber")),
new VegetableMaker("Italian Salad", List.of("Cheese")),
new VegetableMaker("Greek Salad", List.of("Onion")));
Map<String, List<VegetableMaker>> map = dbObjects.stream()
.flatMap(x -> x.getVegetableList().stream().map(y -> new SimpleEntry<>(x, y)))
.collect(Collectors.groupingBy(
Entry::getValue,
Collectors.mapping(Entry::getKey, Collectors.toList())));
System.out.println(map);
Resulting being something like:
{Onion=[Salad, Greek Salad], Cheese=[Italian Salad], Cucumber=[Salad]}
EDIT
This is not much different than what I posted above:
Map<String, Set<VegetableMaker>> result = vegetableMakerList.stream()
.flatMap(x -> x.getKeywords().stream().distinct().map(y -> new SimpleEntry<>(x, y)))
.collect(Collectors.groupingBy(
Entry::getValue,
Collectors.mapping(Entry::getKey, Collectors.toSet())));
final Set<VegetableMaker> vegetableMakers = vegetableMakerDao.findAll();
final Map<String, Set<VegetableMaker>> vegetableMakersContainingKeywordInTheirList = vegetableMakers.stream()
.map(VegetableMaker::getKeywords)
.flatMap(Collection::stream)
.distinct()
.collect(Collectors.toMap(
Function.identity(),
vegetable -> vegetableMakers.stream()
.filter(vegetableMaker -> vegetableMaker.getKeywords().contains(vegetable))
.collect(Collectors.toSet())
));
A bit weak with the concept of lambdas and streams so there might be stuff that really doesn't make any sense but I'll try to convey what I want to happen.
I have a class Invoice where there is an item name, price, and quantity.
I'm having to map the item name and to the total cost (price*quantity).
Though it doesn't work, hope it gives an idea what problem I am having:
invoiceList.stream()
.map(Invoice::getDesc)
.forEach(System.out.println(Invoice::getPrice*Invoice::getQty));
I can already tell the forEach would not work as it's mapping to a variable description(getDesc) and not the Invoice object where I can use it's methods to get other variables.
So, if item=pencil, price=1, qty=12, the output I would want is:
Pencil 12.00
This would be done on multiple Invoice objects.
Also, I am needing to sort them by their total and also filter those above a certain amount, eg. 100. How am I supposed to do that after having them placed in Map ?
if all you want to do is print to the console then it can be done as follows:
invoiceList.forEach(i -> System.out.println(i.getName() + " " + (i.getPrice() * i.getQty())));
If not then read on:
Using the toMap collector
Map<String, Double> result =
invoiceList.stream()
.collect(Collectors.toMap(Invoice::getName,
e -> e.getPrice() * e.getQuantity()));
This basically creates a map where the keys are the Invoice names and the values are the multiplication of the invoice price and quantity for that given Invoice.
Using the groupingBy collector
However, if there can be multiple invoices with the same name then you can use the groupingBy collector along with summingDouble as the downstream collector:
Map<String, Double> result =
invoiceList.stream()
.collect(groupingBy(Invoice::getName,
Collectors.summingDouble(e -> e.getPrice() * e.getQuantity())));
This groups the Invoice's by their names and then for each group sums the result of e.getPrice() * e.getQuantity().
Update:
if you want the toMap version and the result filtered then sorted by value ascending it can be done as follows:
Map<String, Double> result = invoiceList.stream()
.filter(e -> e.getPrice() * e.getQuantity() > 100)
.sorted(Comparator.comparingDouble(e -> e.getPrice() * e.getQuantity()))
.collect(Collectors.toMap(Invoice::getName,
e -> e.getPrice() * e.getQuantity(),
(left, right) -> left,
LinkedHashMap::new));
or with groupingBy approach :
Map<String, Double> result =
invoiceList.stream()
.collect(groupingBy(Invoice::getName,
Collectors.summingDouble(e -> e.getPrice() * e.getQuantity())))
.entrySet()
.stream()
.filter(e -> e.getValue() > 100)
.sorted(Map.Entry.comparingByValue())
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue, (left, right) -> left,
LinkedHashMap::new));
So first, here's the code that I think you were trying to write:
invoiceList.stream()
.forEach(invoice -> System.out.println(invoice.getPrice() * invoice.getQty()));
Now let's look at what's going on here:
Calling .stream() on your list creates an object that has methods available for doing operations on the contents of the list. foreach is one such method that invokes a function on each element. map is another such method, but it returns another stream, where the contents are the contents of your original stream, but each element is replaced by the return value of the function that you pass to map.
If you look at the inside of the foreach call, you can see a lambda. This defines an anonymous function that will be called on each element of your invoiceList. The invoice variable to the left of the -> symbol is bound to each element of the stream, and the expression to the right executed.
I would like to flatten a Map which associates an Integer key to a list of String, without losing the key mapping.
I am curious as though it is possible and useful to do so with stream and lambda.
We start with something like this:
Map<Integer, List<String>> mapFrom = new HashMap<>();
Let's assume that mapFrom is populated somewhere, and looks like:
1: a,b,c
2: d,e,f
etc.
Let's also assume that the values in the lists are unique.
Now, I want to "unfold" it to get a second map like:
a: 1
b: 1
c: 1
d: 2
e: 2
f: 2
etc.
I could do it like this (or very similarly, using foreach):
Map<String, Integer> mapTo = new HashMap<>();
for (Map.Entry<Integer, List<String>> entry: mapFrom.entrySet()) {
for (String s: entry.getValue()) {
mapTo.put(s, entry.getKey());
}
}
Now let's assume that I want to use lambda instead of nested for loops. I would probably do something like this:
Map<String, Integer> mapTo = mapFrom.entrySet().stream().map(e -> {
e.getValue().stream().?
// Here I can iterate on each List,
// but my best try would only give me a flat map for each key,
// that I wouldn't know how to flatten.
}).collect(Collectors.toMap(/*A String value*/,/*An Integer key*/))
I also gave a try to flatMap, but I don't think that it is the right way to go, because although it helps me get rid of the dimensionality issue, I lose the key in the process.
In a nutshell, my two questions are :
Is it possible to use streams and lambda to achieve this?
Is is useful (performance, readability) to do so?
You need to use flatMap to flatten the values into a new stream, but since you still need the original keys for collecting into a Map, you have to map to a temporary object holding key and value, e.g.
Map<String, Integer> mapTo = mapFrom.entrySet().stream()
.flatMap(e->e.getValue().stream()
.map(v->new AbstractMap.SimpleImmutableEntry<>(e.getKey(), v)))
.collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));
The Map.Entry is a stand-in for the nonexistent tuple type, any other type capable of holding two objects of different type is sufficient.
An alternative not requiring these temporary objects, is a custom collector:
Map<String, Integer> mapTo = mapFrom.entrySet().stream().collect(
HashMap::new, (m,e)->e.getValue().forEach(v->m.put(v, e.getKey())), Map::putAll);
This differs from toMap in overwriting duplicate keys silently, whereas toMap without a merger function will throw an exception, if there is a duplicate key. Basically, this custom collector is a parallel capable variant of
Map<String, Integer> mapTo = new HashMap<>();
mapFrom.forEach((k, l) -> l.forEach(v -> mapTo.put(v, k)));
But note that this task wouldn’t benefit from parallel processing, even with a very large input map. Only if there were additional computational intense task within the stream pipeline that could benefit from SMP, there was a chance of getting a benefit from parallel streams. So perhaps, the concise, sequential Collection API solution is preferable.
You should use flatMap as follows:
entrySet.stream()
.flatMap(e -> e.getValue().stream()
.map(s -> new SimpleImmutableEntry(e.getKey(), s)));
SimpleImmutableEntry is a nested class in AbstractMap.
Hope this would do it in simplest way. :))
mapFrom.forEach((key, values) -> values.forEach(value -> mapTo.put(value, key)));
This should work. Please notice that you lost some keys from List.
Map<Integer, List<String>> mapFrom = new HashMap<>();
Map<String, Integer> mapTo = mapFrom.entrySet().stream()
.flatMap(integerListEntry -> integerListEntry.getValue()
.stream()
.map(listItem -> new AbstractMap.SimpleEntry<>(listItem, integerListEntry.getKey())))
.collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));
Same as the previous answers with Java 9:
Map<String, Integer> mapTo = mapFrom.entrySet()
.stream()
.flatMap(entry -> entry.getValue()
.stream()
.map(s -> Map.entry(s, entry.getKey())))
.collect(toMap(Entry::getKey, Entry::getValue));