I have a Map<Person, Long> personToSalary which I would like to map to another Map<String, Long> lastnameToSalary.
Now, this can potentially lead that two Person objects that are normally not equal, will have the equal lastname property and this will cause the duplicate key insertion to a new map. This is OK, as long as I can catch this exception, and throw my own. But not sure how to do so.
Here is the code.
Map<Person, Long> personToSalaray = getMappings();
// lastname to salary
Map<String, Long> personToSalary.entrySet().stream()
.collect(toMap(
e -> e.getKey().getLastname(),
e -> e.getValue()));
While this works, it will potentially throw an exception on duplicate key insertion (same lastname). How to catch it? I can't declare a try-catch inside of toMap.
According to the Javadocs of Collectors.map():
If the mapped keys contains duplicates (according to Object.equals(Object)), an IllegalStateException is thrown when the collection operation is performed. If the mapped keys may have duplicates, use toMap(Function, Function, BinaryOperator) instead.
So the solution should not be catching an exception but using the other toMap overload specifically designed to treat duplicates. The documentation of that method gives an example close to your scenario:
There are multiple ways to deal with collisions between multiple elements mapping to the same key. The other forms of toMap simply use a merge function that throws unconditionally, but you can easily write more flexible merge policies. For example, if you have a stream of Person, and you want to produce a "phone book" mapping name to address, but it is possible that two persons have the same name, you can do as follows to gracefully deals with these collisions, and produce a Map mapping names to a concatenated list of addresses:
Map<String, String> phoneBook
people.stream().collect(toMap(Person::getName,
Person::getAddress,
(s, a) -> s + ", " + a));
use https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#toMap-java.util.function.Function-java.util.function.Function-java.util.function.BinaryOperator-
Map<String, Long> personToSalary.entrySet().stream()
.collect(toMap(
e -> e.getKey().getLastname(),
e -> e.getValue(),
(a, b) -> a // just choose one of the duplicates, or you can put more logic to decide which one you need
));
Related
I have a stream that processes some strings and collects them in a map.
But getting the following exception:
java.lang.IllegalStateException:
Duplicate key test#yahoo.com
(attempted merging values [test#yahoo.com] and [test#yahoo.com])
at java.base/java.util.stream.Collectors.duplicateKeyException(Collectors.java:133)
I'm using the following code:
Map<String, List<String>> map = emails.stream()
.collect(Collectors.toMap(
Function.identity(),
email -> processEmails(email)
));
The flavor of toMap() you're using in your code (which expects only keyMapper and valueMapper) disallow duplicates merely because it's not capable to handle them. And exception message explicitly tells you that.
Judging by the resulting type Map<String, List<String>> and by the exception message which shows strings enclosed in square brackets, it is possible to make the conclusion that processEmails(email) produces a List<String> (although it's not obvious from your description and IMO worth specifying).
There are multiple ways to solve this problem, you can either:
Use this another version of toMap(keyMapper,valueMapper,mergeFunction) which requires the third argument, mergeFunction - a function responsible for resolving duplicates.
Map<String, List<String>> map = emails.stream()
.collect(Collectors.toMap(
Function.identity(),
email -> processEmails(email),
(list1, list2) -> list1 // or { list1.addAll(list2); return list1} depending on the your logic of resolving duplicates you need
));
Make use of the collector groupingBy(classifier,downstream) to preserve all the emails retrieved by processEmails() that are associated with the same key by storing them into a List. As a downstream collector we could utilize a combination of collectors flatMapping() and toList().
Map<String, List<String>> map = emails.stream()
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.flatMapping(email -> processEmails(email).stream(),
Collectors.toList())
));
Note that the later option would make sense only if processEmails() somehow generates different results for the same key, otherwise you would end up with a list of repeated values which doesn't seem to be useful.
But what you definitely shouldn't do in this case is to use distinct(). It'll unnecessarily increase the memory consumption because it eliminates the duplicates by maintaining a LinkedHashSet under the hood. It would be wasteful because you're already using Map which is capable to deal with duplicated keys.
You have duplicate emails. The toMap version you're using explicitly doesn't allow duplicate keys. Use the toMap that takes a merge function. How to merge those processEmails results depends on your business logic.
Alternatively, use distinct() before collecting, because otherwise you'll probably end up sending some people multiple emails.
try using
Collectors.toMap(Function keyFuntion, Function valueFunction, BinaryOperator mergeFunction)
You obviously have to write your own merge logic, a simple mergeFunction could be
(x1, x2) -> x1
I have a list of Profile objects List<Profile> list.
Which I need to convert into a LinkedHashMap<String, String>.
Where object Profile is consisted of:
public class Profile {
private String profileId;
private String firstName;
private String lastName;
}
I have tried the following:
Map<String, String> map = list.stream()
.collect(Collectors.toMap(Profile::getFirstName,
Profile::getLastName));
But it did not work, I'm getting a compilation error:
Incompatible parameter types in method reference expression
Incompatible parameter types in method reference expression
Make sure that you're not using a list of row type as a stream source. I.e. check if the generic type parameter is missing: List list (it has to be List<Profile> list), otherwise all elements of the list as being of type Object and methods from the Profile class would not be accessible.
Collecting into a LinkedHashMap
By default, toMap provides you with a general purpose implementation of the Map (for now it's HashMap but it might change in the future).
In order to collect stream elements into a particular implementation of the Map interface, you need to use a flavor of Collectors.toMap() that expects four arguments:
keyMapper - a mapping function to produce keys,
valueMapper - a mapping function to produce values,
mergeFunction - function that is meant to resolve collisions between value associated with the same key,
mapFactory - a supplier providing a new empty Map into which the results will be inserted.
In the code below, mergeFunction isn't doing anything useful, it just has to be present in order to utilize the version of toMap() that allows to specify the mapFactory.
Map<String, String> map = list.stream()
.collect(Collectors.toMap(
Profile::getFirstName,
Profile::getLastName,
(left, right) -> left,
LinkedHashMap::new));
Note if there could be cases when more than one value gets associated with the same key, you need either to provide a proper implementation of mergeFunction (to peek a particular value or aggregate values, etc.), or use groupingBy() as a collector, which will allow to preserve all values associated with a particular key.
I am doing a group by on a list of Objects as shown in the below code
Map<String, List<InventoryAdjustmentsModel>> buildDrawNumEquipmentMap = equipmentsAndCargoDetails.stream().
collect(Collectors.groupingBy(InventoryAdjustmentsModel :: getBuildDrawNum));
Now I know the values for all the keys would have only one element, so how can I reduce it to just
Map<String, InventoryAdjustmentsModel>
instead of having to iterate through or get the 0th element for all the keys.
You may use the toMap collector with a merge function like this.
Map<String, InventoryAdjustmentsModel> resultMap = equipmentsAndCargoDetails.stream().
collect(Collectors.toMap(InventoryAdjustmentsModel::getBuildDrawNum,
e -> e, (a, b) -> a));
Try it like this. By using toMap you can specify the key and the value. Since you said there were no duplicate keys this does not include the merge method. This means you will get an error if duplicate keys are discovered. Something I presumed you would want to know about.
Map<String, InventoryAdjustmentsModel> buildDrawNumEquipmentMap =
equipmentsAndCargoDetails.stream().
collect(Collectors.toMap(InventoryAdjustmentsModel::getBuildDrawNum,
model->model));
I have a list of class say ProductDto
public class ProductDto {
private String Id;
private String status;
private Booker booker;
private String category;
private String type;
}
I want to have a Map as below:-
Map<String,Map<String,Map<String,Booker>>
The properties are to be mapped as below:
Map<status,Map<category,Map<type,Booker>
I know one level of grouping could be done easily without any hassles using Collectors.groupingBy.
I tried to use this for nested level but it failed for me when same values started coming for fields that are keys.
My code is something like below:-
list.stream()
.collect(Collectors.groupingBy(
(FenergoProductDto productDto) ->
productDto.getStatus()
,
Collectors.toMap(k -> k.getProductCategory(), fProductDto -> {
Map<String, Booker> productTypeMap = new ProductTypes();
productTypeMap.put(fProductDto.getProductTypeName(),
createBooker(fProductDto.getBookingEntityName()));
return productTypeMap;
})
));
If anyone knows a good approach to do this by using streams, please share!
Abstract / Brief discussion
Having a map of maps of maps is questionable when seen from an object-oriented prespective, as it might seem that you're lacking some abstraction (i.e. you could create a class Result that encapsulates the results of the nested grouping). However, it's perfectly reasonable when considered exclusively from a pure data-oriented approach.
So here I present two approaches: the first one is purely data-oriented (with nested groupingBy calls, hence nested maps), while the second one is more OO-friendly and makes a better job at abstracting the grouping criteria. Just pick the one which better represents your intentions and coding standards/traditions and, more importantly, the one you most like.
Data-oriented approach
For the first approach, you can just nest the groupingBy calls:
Map<String, Map<String, Map<String, List<Booker>>>> result = list.stream()
.collect(Collectors.groupingBy(ProductDto::getStatus,
Collectors.groupingBy(ProductDto::getCategory,
Collectors.groupingBy(ProductDto::getType,
Collectors.mapping(
ProductDto::getBooker,
Collectors.toList())))));
As you see, the result is a Map<String, Map<String, Map<String, List<Booker>>>>. This is because there might be more than one ProductDto instance with the same (status, category, type) combination.
Also, as you need Booker instances instead of ProductDto instances, I'm adapting the last groupingBy collector so that it returns Bookers instead of productDtos.
About reduction
If you need to have only one Booker instance instead of a List<Booker> as the value of the innermost map, you would need a way to reduce Booker instances, i.e. convert many instances into one by means of an associative operation (accumulating the sum of some attribute being the most common one).
Object-oriented friendly approach
For the second approach, having a Map<String, Map<String, Map<String, List<Booker>>>> might be seen as bad practice or even as pure evil. So, instead of having a map of maps of maps of lists, you could have only one map of lists whose keys represent the combination of the 3 properties you want to group by.
The easiest way to do this is to use a List as the key, as lists already provide hashCode and equals implementations:
Map<List<String>, List<Booker>> result = list.stream()
.collect(Collectors.groupingBy(
dto -> Arrays.asList(dto.getStatus(), dto.getCategory(), dto.getType()),
Collectors.mapping(
ProductDto::getBooker,
Collectors.toList())))));
If you are on Java 9+, you can use List.of instead of Arrays.asList, as List.of returns a fully immutable and highly optimized list.
nested groupingBy questions and solutions:
q. print all male and female dept-wise(nested groupingBy):
ans:
employeeList.stream().collect(Collectors.groupingBy(Employee::getDepartment,Collectors.groupingBy(Employee::getGender)))
.entrySet().stream().forEach(System.out::println)
q. print the employees more than 25 and not - male and female - dept-wise
ans:
employeeList.stream().collect(
Collectors.groupingBy(Employee::getDepartment, Collectors.groupingBy(Employee::getGender, Collectors.partitioningBy(emp -> emp.getAge() > 25))))
.entrySet().stream().forEach(System.out::println);
q. eldest male and female from each department
ans:
employeeList.stream().collect(Collectors.groupingBy(Employee::getDepartment,Collectors.groupingBy(Employee::getGender,Collectors.maxBy(Comparator.comparing(Employee::getAge)))))
.entrySet().stream().forEach(System.out::println);
some more helpful questions #:
[1]: https://www.youtube.com/watch?v=AFmyV43UBgc
I want to convert List of Objects to Map, where Map's key and value located as attributes inside Object in List.
Here Java 7 snippet of such convertation:
private Map<String, Child> getChildren(List<Family> families ) {
Map<String, Child> convertedMap = new HashMap<String, Child>();
for (Family family : families) {
convertedMap.put(family.getId(), family.getParent().getChild());
}
return convertedMap;
}
It should be something similar to...
Map<String, Child> m = families.stream()
.collect(Collectors.toMap(Family::getId, f -> f.getParent().getChild()));
Jason gave a decent answer (+1) but I should point out that it has different semantics from the OP's Java 7 code. The issue concerns the behavior if two family instances in the input list have duplicate IDs. Maybe they're guaranteed unique, in which case there is no difference. If there are duplicates, though, with the OP's original code, a Family later in the list will overwrite the map entry for a Family earlier in the list that has the same ID.
With Jason's code (shown below, slightly modified):
Map<String, Child> getChildren(List<Family> families) {
return families.stream()
.collect(Collectors.toMap(Family::getId, f -> f.getParent().getChild()));
}
the Collectors.toMap operation will throw IllegalStateException if there are any duplicate keys. This is somewhat unpleasant, but at least it notifies you that there are duplicates instead of potentially losing data silently. The rule for Collectors.toMap(keyMapper, valueMapper) is that you need to be sure that the key mapper function returns a unique key for every element of the stream.
What you need to do about this -- if anything -- depends on the problem domain. One possibility is to use the three-arg version: Collectors.toMap(keyMapper, valueMapper, mergeFunction). This specifies an extra function that gets called in the case of duplicates. If you want to have later entries overwrite earlier ones (matching the original Java 7 code), you'd do this:
Map<String, Child> getChildren(List<Family> families) {
return families.stream()
.collect(Collectors.toMap(Family::getId, f -> f.getParent().getChild(),
(child1, child2) -> child2));
}
An alternative would be to build up a list of children for each family instead of having just one child. You could write a more complicated merging function that created a list for the first child and appended to this list for the second and subsequent children. This is so common that there is a special groupingBy collector that does this automatically. By itself this would produce a list of families grouped by ID. We don't want a list of families but instead we want a list of children, so we add a downstream mapping operation to map from family to child, and then collect the children into a list. The code would look like this:
Map<String, List<Child>> getChildren(List<Family> families) {
return families.stream()
.collect(Collectors.groupingBy(Family::getId,
Collectors.mapping(f -> f.getParent().getChild(),
Collectors.toList())));
}
Note that the return type has changed from Map<String, Child> to Map<String, List<Child>>.