java stream for grouping by multiple keys [duplicate] - java

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

Kotlin/Java functional and immutable way of collecting a map in a map

Currently i'm reading a file through the Java API and adding items to a map through the method foreach which forces me to make use of the mutablemap. Is there a way of collecting the items without the mutablemap? I know there is a method collect but I couldn't get it working.
the current way:
val result = mutableMapOf<Int, MutableMap<Int, Double>>()
Files.lines(Paths.get(folderPath))
.map { line -> line.split(",") }
.map { items -> Triple(items[0].toInt(), items[1].toInt(), items[2].toDouble()) }
.forEach { (id, article, rating) ->
if (result.containsKey(id))
result[id]!!.put(article, rating)
else
result.put(id, mutableMapOf(Pair(article, rating)))
}
EDIT:
my goal is to merge the triple objects based on the first value of the triple.
So a scenario would be two triple objects(1, 2, 5.5) and (1,3, 5.5). The first value of the triple is the user id, the second is the article id and the third is the rating of the article.
after merging there would be one single entry in the map containing the first key = the user id, the first value of the triple, and the value would be a map containing the articles which the user rated.
It is currently working how I want it to work, but I was curious if there was a more functional way of solving this.
You could do this:
val result: Map<Int, Map<Int, Double>> = Files.lines(Paths.get(folderPath))
.map { it.split(",") }
.collect(groupingBy({ it[0].toInt() },
toMap<List<String>, Int, Double>({ it[1].toInt() }) { it[2].toDouble() }))
The compiler didn't seem to be able to infer the types for toMap, so I added a type hint there.
I will point out that Kotlin doesn't have an immutable Map implementation. The default returned by mapOf is java.util.LinkedhashMap which is mutable. It's just that the Map interface doesn't expose the mutating methods (like put, putAll). And you can freely assign a MutableMap<Int, MutableMap<Int, Double>> to a Map<Int, Map<Int, Double>>, which is also what I'm doing here with the type annotation on result, I'm forcing the static type to be the immutable Map.
I am not comfortable enough with Kotlin, but it seems that you should be using a simple : Collectors.collectingAndThen. Something like:
System.out.println(Stream.of(1, 2, 3, 1)
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(Function.identity(), Collectors.counting()),
Collections::unmodifiableMap)));
The idea is that you collect to a mutable map and then wrap that into an ImmutableMap.
If by the way you have guava on the class path - it has build collectors that collect to an ImmutableMap.

Is there a data structure in Java that can hold 4 or more values

Is there a data structure in Java which can hold more than 4 values?
So something along the lines of
Map<String, String, String, String>;
This is needed to be able to reduce the number of if else statements I have. I would like to be able to do the following.
check if the data structure contains an element which matches a certain value, if it does then it assigns a link(which is string) to a variable and then adds a code and message to another variable which is related to that link.
if not is there a good workout around to achieve this?
Is there a data structure in Java which can hold more than 4 values?
There are lots of them.
The simplest is probably String[] which can hold 4 strings if you instantiate it like this:
new String[4]
And other Answers give other data structures that might meet your actual (i.e. unstated) requirements.
However, it is probably possible ... let alone sensible ... for us to enumerate all of the possible data structures that can meet your stated requirement.
Hint: you should try to explain how this data structure needs to work.
Hint 2: "the lines of Map<String, String, String, String>" does not help us understand your real requirement because we don't know what you mean by that.
UPDATE - Your explanation is still extremely vague, but I think you need something like this:
Map<String, MyRecord>;
public class MyRecord {
private String link;
private String code;
private String message;
// add constructor, getters, setters as required
}
There is nothing in the standard libraries, but Guava has a nice implementation; called
Multimap
If Guava is not an option in your environment, you will have to re-invent the wheel though.
Use can use MultiMap on Apache,
A MultiMap is a Map with slightly different semantics. Putting a value into the map will add the value to a Collection at that key. Getting a value will return a Collection, holding all the values put to that key
MultiMap mhm = new MultiValueMap();
mhm.put(key, "A");
mhm.put(key, "B");
mhm.put(key, "C");
Collection coll = (Collection) mhm.get(key);
Use Map of map:
Map<String, Map<String, Map<String, String>>>
Reading your question, It seams like you simply need a 'key-value' pair. Key being a 'String', which you have referred as 'a certain value' in your question. Value is a kind of wrapper object wrapping three Strings which you have referred as 'link, code and message'.
I suggest you can simply use
HashMap < String, Wrapper > map;
You can create a class ' Wrapper.java' which can contain three Strings, as instance fields
String link,code,message;
You can instantiate these fields in constructor and later can retrieve them using getter methods or also can have setters to set the values you need. You can provide a better contextual name to the class 'Wrapper.java'.

Creating a map of maps from a map Java 8

I have a Map<String, List<SomeClassA>> that I'm trying to convert into a Map<String, Map<String, List<SomeWrapperOfClassA>>> and I'm just having so much trouble wrapping my head around how to do this.
Really, all the information needed to create the map should be in the objects of type SomeClassA - say
SomeClassA:
String attributeA;
String attributeB;
SomeClassB someOtherInfo;
SomeClassB:
String attribute C;
And I want to say it's a map based on this:
Map<attributeA's values,Map<attribute C vals, List SomeWrapperOfClassA>>
where the list is only of SomeWrapperClassA that has those values of attributeA and attributeB. I was thinking it might have to do with groupingBy, but I'm not too familiar with how to do it in such a way that its nested like this.
(or for the sake of simplicity, any help just getting the original list of SomeClassA into a Map<String, Map<String, List<SomeClassA>>> would already be a huge help.
I haven't quite gotten the hang of Java 8 and the more complex streaming concepts yet, so some help would be greatly appreciated. I'm only familiar with the basics.
I am not sure what you meant by:
Map<String, Map<String, List<SomeClassA>>>, what is the key here to group by? in general if you want to group List<SomeClassA> by say attributeA from SomeClassA you can do this:
List<SomeClassA>.stream().collect(Collectors.groupingBy(someClsA-> someClsA.getAttributeA()));
If I understand your questions correctly I would express it as follows:
Given:
class ClassA {
public String getA();
public ClassB getB();
}
class ClassB {
public String getC();
}
Map<String, List<ClassA>> input;
How would I create a Map<String, Map<String, List<ClassB>> where the key of the inner map is the result of getA?
If that's correct then you aren't looking to change the keys of the outer map at all. That makes it a good candidate for Map.replaceAll. For clarity I've split the collector into a separate method so it's clear what's happening:
input.replaceAll(listA.stream().collect(mapCollector()));
private Collector<A, ?, Map<String, List<B>> mapCollector() {
return Collectors.groupingBy(ClassA::getA,
Collectors.mapping(ClassA::getB,
Collectors.toList()));
}
Explaining the method it says group As by getA as key, then collect those As by mapping using getB then putting in a list.
If you particularly want a new map (rather than changing the original one) then:
Map<String, Map<String, List<ClassB>>> output = input.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().stream().collect(mapCollector()));
It’s not entirely clear what you want to achieve. Apparently, the keys of your source map are irrelevant and you only want to process the SomeClassA instances contained in the value lists. So, the first step is to stream over the map’s values, i.e. sourceMap.values().stream(), then flatten this Stream<List<SomeClassA>> to a Stream<SomeClassA> via flatMap(List::stream).
Grouping these SomeClassA instances by one of their properties works indeed straight-forwardly via groupingBy, which needs another nested groupingBy to group each group further by “Attribute C”:
Map<String, Map<String, List<SomeClassA>>> resultMap
= sourceMap.values().stream().flatMap(List::stream)
.collect(Collectors.groupingBy(SomeClassA::getAttributeA,
Collectors.groupingBy(a->a.getSomeOtherInfo().getAttributeC())));
To convert the SomeClassA instances to SomeWrapperClassA instances, we need another nested collector for the innermost groupingBy; the mapping collector allows to map the values before transferring to another collector, which has to be the toList() collector, which was formerly implicit.
Now, it might be better to start using import static java.util.stream.Collectors.*;
Map<String, Map<String, List<SomeWrapperClassA>>> resultMap
= sourceMap.values().stream().flatMap(List::stream)
.collect(groupingBy(SomeClassA::getAttributeA, groupingBy(
a -> a.getSomeOtherInfo().getAttributeC(),
mapping(a->new SomeWrapperClassA(a.getAttributeA(),a.getAttributeB()), toList()))));

Map<Integer, List<courseCLS>> to Map<Integer, List<Course>>

I have an interface Course and a class CourseCLS implements Course .
First I tried to group CourseCLS objects by the same numbers. I'm not sure if it's correct or not.
Map<Integer, List<CourseCLS>> first =
courses.values().stream()
.collect(Collectors.groupingBy(c -> c.getNumber()));
Now I need to change Map<Integer, List<CourseCLS>> into Map<Integer, List<Course>>, and then return it.
Any ideas how to do it?
Just solved adding this to groupingBy:
groupingBy(c -> c.overHeadPercentage(),
Collectors.mapping(p -> (Course) p, Collectors.toList()))
You are trying to solve a problem which doesn’t exist. Just write
Map<Integer, List<Course>> first =
courses.values().stream()
.collect(Collectors.groupingBy(c -> c.getNumber()));
Since the Stream elements are instances of CourseCLS which implements Course, you can collect them into a List<Course> without the need for any additional action.
The only possible problem could arise, if the Course interface doesn’t have the getNumber method, but only the concrete CourseCLS class, as the collector above is now collecting Course instances. But even this can be solved without any mapping step:
Map<Integer, List<Course>> first =
courses.values().stream()
.collect(Collectors.groupingBy(c -> c.getNumber(), Collectors.toList()));
Since groupingBy allows the downstream collector to collect a broader type than itself collects, you can combine a toList() collector collecting Course instances with a groupingBy collector which collects CourseCLS instances. The type inference of Java 8 detects the validity of this construct.
Since CourseCLS implements Course you can just make a new Map with Course as value, it will accept any instance of Course interface so will accept CourseCLS.
So go though your first map and add every K,V pair to the result Map

How to create Map in LambdaJ where value is Collection?

I know how to make Map<String, Car> but how to make Map<String, List<Car>> in lambdaj?
This is code I want to write in LambdaJ:
Map<String, List<Car>> driverCarsMap = new HashMap<String, List<Car>>();
for (Car car : cars)
{
String driver = car.getDriver();
if (!driverCarsMap.containsKey(driver))
driverCarsMap.put(driver, new ArrayList<Car>());
driverCarsMap.get(driver).add(car);
}
Unfortunately the code:
Map<String, List<Car>> driverCarsMap = index(cars, on(Car.class).getDriver());
creates the Map, but value is not being extended but overwritten. So, de facto we do note have a List but single object.
What you want to do is similar to LambdaJ index method. The problem with index is its limitation, its documentation says:
The former index feature is enough in many cases, but it has at least
2 big limitations: it doesn't manage conflicts if 2 or more objects
should be indexed on the same key value and it doesn't easily allow to
index the objects in a multilevel hierarchy.
To overcome these constraints lambdaj provides a feature to
(optionally hierarchically) group objects on the value of their
properties. As usual the preferred (statically typed) way in lambdaj
to choose an object's property is via the on construct, so to group a
List of Persons based on their ages it is sufficient to write:
Group group = group(meAndMyFriends, by(on(Person.class).getAge()));
So, what you have to use is LambdaJ groups. For your example, it would be:
Group<Car> group = group(cars, by(on(Car.class).getDriver()));
You can take a look at grouping items here:
https://code.google.com/p/lambdaj/wiki/LambdajFeatures#Grouping_items

Categories