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
Related
I have a stream of objects similar to this previous question, however, instead of ignoring duplicate values, I would like to remove any values from that stream beforehand and print them out.
For example, from this snippet:
Map<String, String> phoneBook = people.stream()
.collect(toMap(Person::getName,
Person::getAddress));
If there were duplicate entries, it would cause a java.lang.IllegalStateException: Duplicate key error to be thrown.
The solution proposed in that question used a mergeFunction to keep the first entry if a collision was found.
Map<String, String> phoneBook =
people.stream()
.collect(Collectors.toMap(
Person::getName,
Person::getAddress,
(address1, address2) -> {
System.out.println("duplicate key found!");
return address1;
}
));
Instead of keeping the first entry, if there is a collision from a duplicate key in the stream, I want to know which value caused the collision and make sure that there are no occurrences of that value within the resulting map.
I.e. if "Bob" appeared three times in the stream, it should not be in the map even once.
In the process of creating that map, I would like to filter out any duplicate names and record them some way.
I want to make sure that when creating the list there can be no duplicate entry and for there to be some way to know which entries had duplicate keys in incoming stream. I was thinking about using groupingBy and filter beforehand to find the duplicate keys, but I am not sure what the best way to do it is.
I would like to remove any values from that stream beforehand.
As #JimGarrison has pointed out, preprocessing the data doesn't make sense.
You can't know it in advance whether a name is unique or not until the all data set has been processed.
Another thing that you have to consider that inside the stream pipeline (before the collector) you have knowledge on what data has been encountered previously. Because results of intermediate operations should not depend on any state.
In case if you are thinking that streams are acting like a sequence of loops and therefore assuming that it's possible to preprocess stream elements before collecting them, that's not correct. Elements of the stream pipeline are being processed lazily one at a time. I.e. all the operations in the pipeline will get applied on a single element and each operation will be applied only if it's needed (that's what laziness means).
For more information, have a look at this tutorial and API documentation
Implementations
You can segregate unique values and duplicates in a single stream statement by utilizing Collectors.teeing() and a custom object that will contain separate collections of duplicated and unique entries of the phone book.
Since the primarily function of this object only to carry the data I've implemented it as Java 16 record.
public record FilteredPhoneBook(Map<String, String> uniquePersonsAddressByName,
List<String> duplicatedNames) {}
Collector teeing() expects three arguments: two collectors and a function that merges the results produced by both collectors.
The map generated by the groupingBy() in conjunction with counting(), is meant to determine duplicated names.
Since there's no point to processing the data, toMap() which is used as the second collector will create a map containing all names.
When both collectors will hand out their results to the merger function, it will take care of removing the duplicates.
public static FilteredPhoneBook getFilteredPhoneBook(Collection<Person> people) {
return people.stream()
.collect(Collectors.teeing(
Collectors.groupingBy(Person::getName, Collectors.counting()), // intermediate Map<String, Long>
Collectors.toMap( // intermediate Map<String, String>
Person::getName,
Person::getAddress,
(left, right) -> left),
(Map<String, Long> countByName, Map<String, String> addressByName) -> {
countByName.values().removeIf(count -> count == 1); // removing unique names
addressByName.keySet().removeAll(countByName.keySet()); // removing all duplicates
return new FilteredPhoneBook(addressByName, new ArrayList<>(countByName.keySet()));
}
));
}
Another way to address this problem to utilize Map<String,Boolean> as the mean of discovering duplicates, as #Holger have suggested.
With the first collector will be written using toMap(). And it will associate true with a key that has been encountered only once, and its mergeFunction will assign the value of false if at least one duplicate was found.
The rest logic remains the same.
public static FilteredPhoneBook getFilteredPhoneBook(Collection<Person> people) {
return people.stream()
.collect(Collectors.teeing(
Collectors.toMap( // intermediate Map<String, Boolean>
Person::getName,
person -> true, // not proved to be a duplicate and initially considered unique
(left, right) -> false), // is a duplicate
Collectors.toMap( // intermediate Map<String, String>
Person::getName,
Person::getAddress,
(left, right) -> left),
(Map<String, Boolean> isUniqueByName, Map<String, String> addressByName) -> {
isUniqueByName.values().removeIf(Boolean::booleanValue); // removing unique names
addressByName.keySet().removeAll(isUniqueByName.keySet()); // removing all duplicates
return new FilteredPhoneBook(addressByName, new ArrayList<>(isUniqueByName.keySet()));
}
));
}
main() - demo
public static void main(String[] args) {
List<Person> people = List.of(
new Person("Alise", "address1"),
new Person("Bob", "address2"),
new Person("Bob", "address3"),
new Person("Carol", "address4"),
new Person("Bob", "address5")
);
FilteredPhoneBook filteredPhoneBook = getFilteredPhoneBook(people);
System.out.println("Unique entries:");
filteredPhoneBook.uniquePersonsAddressByName.forEach((k, v) -> System.out.println(k + " : " + v));
System.out.println("\nDuplicates:");
filteredPhoneBook.duplicatedNames().forEach(System.out::println);
}
Output
Unique entries:
Alise : address1
Carol : address4
Duplicates:
Bob
You can't know which keys are duplicates until you have processed the entire input stream. Therefore, any pre-processing step has to make a complete pass of the input before your main logic, which is wasteful.
An alternate approach could be:
Use the merge function to insert a dummy value for the offending key
At the same time, insert the offending key into a Set<K>
After the input stream is processed, iterate over the Set<K> to remove offending keys from the primary map.
In mathematical terms you want to partition your grouped aggregate and handle both parts separately.
Map<String, String> makePhoneBook(Collection<Person> people) {
Map<Boolean, List<Person>> phoneBook = people.stream()
.collect(Collectors.groupingBy(Person::getName))
.values()
.stream()
.collect(Collectors.partitioningBy(list -> list.size() > 1,
Collectors.mapping(r -> r.get(0),
Collectors.toList())));
// handle duplicates
phoneBook.get(true)
.forEach(x -> System.out.println("duplicate found " + x));
return phoneBook.get(false).stream()
.collect(Collectors.toMap(
Person::getName,
Person::getAddress));
}
I would like to build a Map using the Stream & Lambda couple.
I've tried many ways but I'm stucked. Here's the classic Java code to do it using both Stream/Lambda and classic loops.
Map<Entity, List<Funder>> initMap = new HashMap<>();
List<Entity> entities = pprsToBeApproved.stream()
.map(fr -> fr.getBuyerIdentification().getBuyer().getEntity())
.distinct()
.collect(Collectors.toList());
for(Entity entity : entities) {
List<Funder> funders = pprsToBeApproved.stream()
.filter(fr -> fr.getBuyerIdentification().getBuyer().getEntity().equals(entity))
.map(fr -> fr.getDocuments().get(0).getFunder())
.distinct()
.collect(Collectors.toList());
initMap.put(entity, funders);
}
As you can see, I only know how to collect in a list, but I just can't do the same with a map. That's why I have to stream my list again to build a second list to, finally, put all together in a map.
I've also tried the 'collect.groupingBy' statement as it should too produce a map, but I failed.
It seems you want to map whatever is on the pprsToBeApproved list to your Funder instances, grouping them by buyer Entity.
You can do it as follows:
Map<Entity, List<Funder>> initMap = pprsToBeApproved.stream()
.collect(Collectors.groupingBy(
fr -> fr.getBuyerIdentification().getBuyer().getEntity(), // group by this
Collectors.mapping(
fr -> fr.getDocuments().get(0).getFunder(), // mapping each element to this
Collectors.toList()))); // and putting them in a list
If you don't want duplicate funders for a particular entity, you can collect to a map of sets instead:
Map<Entity, Set<Funder>> initMap = pprsToBeApproved.stream()
.collect(Collectors.groupingBy(
fr -> fr.getBuyerIdentification().getBuyer().getEntity(),
Collectors.mapping(
fr -> fr.getDocuments().get(0).getFunder(),
Collectors.toSet())));
This uses Collectors.groupingBy along with Collectors.mapping.
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.
Map is Map<String, List<User>> and List is List<User>. I want to use
Map<String,List<User>> newMap = oldMap.stream()
.filter(u ->userList.stream()
.filter(ul ->ul.getName().equalsIgnoreCase(u.getKey()).count()>0))
.collect(Collectors.toMap(u.getKey, u.getVaule()));
can't change to new Map. Why?
There are several problems with your code:
Map does not have a stream(): its entry set does, so you need to call entrySet() first.
There are a couple of misplaced parentheses
Collectors.toMap code is incorrect: you need to use the lambda u -> u.getKey() (or the method-reference Map.Entry::getKey) and not just the expression u.getKey(). Also, you mispelled getValue().
This would be a corrected code:
Map<String, List<User>> newMap =
oldMap.entrySet()
.stream()
.filter(u -> userList.stream()
.filter(ul ->ul.getName().equalsIgnoreCase(u.getKey())).count() > 0
).collect(Collectors.toMap(u -> u.getKey(), u -> u.getValue()));
But a couple of notes here:
You are filtering only to see if the count is greater than 0: instead you could just use anyMatch(predicate). This is a short-cuiting terminal operation that returns true if the predicate is true for at least one of the elements in the Stream. This has also the advantage that this operation might not process all the elements in the Stream (when filtering does)
It is inefficient since you are traversing the userList every time you need to filter a Stream element. It would be better to use a Set which has O(1) lookup (so first you would convert your userList into a userSet, transforming the username in lower-case, and then you would search this set for a lower-case value username).
This would be a more performant code:
Set<String> userNameSet = userList.stream().map(u -> u.getName().toLowerCase(Locale.ROOT)).collect(toSet());
Map<String,List<User>> newMap =
oldMap.entrySet()
.stream()
.filter(u -> userNameSet.contains(u.getKey().toLowerCase(Locale.ROOT)))
.collect(Collectors.toMap(u -> u.getKey(), u -> u.getValue()));
Perhaps you intended to create a Stream of the entry Set of the input Map.
Map<String,List<User>> newMap =
oldMap.entrySet().stream()
.filter(u ->userList.stream().filter(ul ->ul.getName().equalsIgnoreCase(u.getKey())).count()>0)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
This would create a Map that retains the entries of the original Map whose keys equal the name of at least one of the members of userList (ignoring case).
I have Map<Integer,Doctor> docLib=new HashMap<>(); to save class of Doctor.
Class Doctor has methods:getSpecialization() return a String,
getPatients() to return a collection of class Person.
In the main method, I type:
public Map<String,Set<Person>> getPatientsPerSpecialization(){
Map<String,Set<Person>> res=this.docLib.entrySet().stream().
map(d->d.getValue()).
collect(groupingBy(d->d.getSpecialization(),
d.getPatients()) //error
);
return res;
}
As you can see, I have problem with groupingBy,I try to send the same value d to the method, but it's wrong.
How to solve this?
You need a second Collector for that mapping :
public Map<String,Set<Person>> getPatientsPerSpecialization(){
return this.docLib
.values()
.stream()
.collect(Colectors.groupingBy(Doctor::getSpecialization,
Collectors.mapping(Doctor::getPatients,toSet()))
);
}
EDIT:
I think my original answer may be wrong (it's hard to say without being able to test it). Since Doctor::getPatients returns a Collection, I think my code may return a Map<String,Set<Collection<Person>>> instead of the desired Map<String,Set<Person>>.
The easiest way to overcome that is to iterate over that Map again to produce the desired Map :
public Map<String,Set<Person>> getPatientsPerSpecialization(){
return this.docLib
.values()
.stream()
.collect(Colectors.groupingBy(Doctor::getSpecialization,
Collectors.mapping(Doctor::getPatients,toSet()))
)
.entrySet()
.stream()
.collect (Collectors.toMap (e -> e.getKey(),
e -> e.getValue().stream().flatMap(c -> c.stream()).collect(Collectors.toSet()))
);
}
Perhaps there's a way to get the same result with a single Stream pipeline, but I can't see it right now.
Instead of groupingBy, you could use toMap:
public Map<String, Set<Person>> getPatientsPerSpecialization() {
return docLib.values()
.stream()
.collect(toMap(Doctor::getSpecialization,
d -> new HashSet<>(d.getPatients()),
(p1, p2) -> Stream.concat(p1.stream(), p2.stream()).collect(toSet())));
}
What it does is that it groups the doctors per specialization and map each one to a set of the patients it has (so a Map<String, Set<Person>>).
If, when collecting the data from the pipeline, you encounter a doctor with a specialization that is already stored as a key in the map, you use the merge function to produce a new set of values with both sets (the set that is already stored as a value for the key, and the set that you want to associate with the key).