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
Related
I want to do a "group-by" on arrayList of HashMap Data structure. As my data is not fixed, so I don't have any fixed classes.
Data is shown as below.
[{"name":"laxman","state":"Karnataka","Mobile":9034782882},
{"name":"rahul","state":"Kerala","Mobile":9034782882},
{"name":"laxman","state":"karnataka","Mobile":9034782882},
{"name":"ram","state":"delhi","Mobile":9034782882}]
The above keys are not fixed, So, I can't have classes for it.
Data and formulas will be dynamical. But for now, I am taking this example to understand Stream.Collector on this data.
Now, I want to get the count on basis of name and state,
So basically I want to group-by on name and state and want to get count.
I tried to use Stream.Collector but am not able to achieve what I want.
You can accomplish this with Collectors.groupingBy, using a List as the key of the returned Map:
Map<List<String>, Long> result = yourListOfMaps.stream()
.collect(Collectors.groupingBy(
m -> Arrays.asList(String.valueOf(m.get("name")), String.valueOf(m.get("state"))),
Collectors.counting()));
This works well because all implementations of List in Java implement hashCode and equals consistently, which is a must for every class that is to be used as the key of any Map implementation.
You have to do groupingBy twice once on the key and once again on the value.
Map<String, Map<Object, Long>> map = listOfMap.stream().flatMap(a -> a.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry<String, String>::getKey,
Collectors.groupingBy(Map.Entry::getValue, Collectors.counting())));
Output
{mobile={9034782882=4}, name={rahul=1, laxman=2, ram=1}, state={Karnataka=2, delhi=1, Kerala=1}}
I have a list of entries ,where entry has studentId and subjectId attributes.
List<Candidate> candidates
class Candidate {
...
String studentId;
String subjectId;
}
The objective is to derive a map of subjectId to list of studentIds,for those subjects which have been subscribed to by MORE than one student.
I can obviously create a temporary map by iterating over the candidates(a big count),remove single entries later - which seems a costly route.
Any other suggestions ?
We are using Java 1.7
Following this answer (corrected and completed) :
Map<String, Integer> map = candidates.stream()
.collect(Collectors.groupingBy(Candidate::getSubjectId))
.entrySet().stream().filter(x -> x.getValue().size()>1)
.collect(Collectors.toMap(x -> x.getKey(), x -> x.getValue().size()));
You could also use candidates.parallelStream().
When you say 'costly', it's not exactly clear what you mean. - but I'll make an attempt.
With Java8, you can use the groupingBy construct to create a
Map<String, List<Candidate> groupedResults = candidates.stream().collect(Collectors.groupingBy(Candidate::getSubjectId));
and then simply filter out the entries where size <= 1. Rather simple and since it uses streams, memory efficiency should not be an issue.
For example, is it generally good to have a structure like :
Map <Object, Map>
In my case, at first sight I do need some kind of structure that would have for example mapping String -> Map<...>,
or it is weird and I should solve the problem somehow else?
It has a few code smells for me.
The inner Map does not specify the generic types.
It says you need to do two look ups. I would prefer a composite key and a single lookup on a flattened map.
Adding to this structure must be overly complicated.
The key is object, not strongly typed.
So I would prefer to see:
Map<CompositeKey, ValueType>
Where CompositeKey is a class with correct equality implementation made up of the first and second keys. and ValueType is not a Map.
So for example, instead of nested maps:
A -> { 1 -> "ABC",
2 -> "DEF"},
B -> { 1 -> "abc",
3 -> "def"}
You have a flat map and a composite key:
(A,1) -> "ABC",
(A,2) -> "DEF",
(B,1) -> "abc",
(B,3) -> "def",
It is not inherently good to design your data structure this way. It also is not necessarily bad.
If you will be doing lookup on the map object I would consider using a HashMap to improve the execution time.
From #Rafael Osipov, In cases involving concurrency consider using ConcurrentHashMap, for data integrity.
There's nothing at all wrong with having a map as the value in a map. It's a fairly common way of implementing fast lookup using multiple keys.
For example, you are storing a large number of Student records:
class Student
private String name;
private int age;
private School school;
}
You could store these in a List<Student> but if you want to find, say, all students at a certain school who are 16 years old it might be better to be able to say:
Map<School,Map<Integer,List<Student>>> index;
List<Student> subset = index.get(School.STATE_HIGH).get(16);
Note that there are other ways of doing the same thing such as having a separate composite key type (in my example above, representing school and age together). Also keep in mind that if your primary purpose in using a map is efficiency then, in many cases, you are probably optimising prematurely. Unless the map is storing many millions of values or you are going to be accessing them many thousands of times a second then you might want to consider a simple list that you search through.
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>>.
So say I have a TreeMap<MyDataType, Integer>, where MyDataType is an object that contains a String and a Long. I want to check if the TreeMap contains a key that has a certain String; however, the Long associated with the object does not matter to me. For instance, my TreeMap could look like this:
{MyDataType: ["Tom", 1L] -> 1, MyDataType: ["Billy", 3L] -> 1, MyDataType: ["Ryan", 8L] -> 1}
I want to see if the TreeMap contains a Key (of type MyDataType) whose String value is "Billy". I can think of two ways to do this:
(1) iterate through the TreeMap one by one, checking the String of each MyDataType key.
(2) write a new class that extends TreeMap<MyDataType, Integer> and write a new containsKeyWithStringValue(String toCheck) that specifically does what I want it to do.
Are there any other more concise ways?
I would create a TreeMap<String, Integer> that will map "Tom" directly to 2, "Billy" to 5, etc.
Basically, you are asking if there is a more concise way of doing this (not more efficient, more scalable, etctera)
I think that the answer is no.
The standard Java Map API doesn't provide any mechanisms for querying a map ... apart from get.
The Guava libraries include support for functional-style programming, including stuff for filtering the keys, values and entries of a Map; see here. However, when you include all the boilerplate that is necessary to implement a Guava predicate, it is doubtful that it will be more concise than iterating and testing the entries by hand.
You also have a requirement that an exception is thrown on a "miss"; i.e. when the String / Integer don't match an entry. With that constraint, the answer is definitely No. Collection APIs are generally defined to return null if there is a "miss" because creating and throwing exceptions is relatively expensive in Java.
With Java 8, the answer to the first part is likely to change because the new lambda and related features will make functional-style programming easier. But this won't address your requirement that misses should result in exceptions.
Finally, I don't like your idea of extending TreeMap with custom methods. I think you should either wrap the class, or implement the "extended functionality" as a static helper method. This is not an objective reason, but extending does "feel right" to me in this situation.