Grouping by and map value - java

I'm trying to do grouping by (to map) and then transform list of values to different list.
I have List of DistrictDocuments:
List<DistrictDocument> docs = new ArrayList<>();
Then I'm streaming over it and grouping it by city:
Map<String, List<DistrictDocument>> collect = docs.stream()
.collect(Collectors.groupingBy(DistrictDocument::getCity));
I also have a method which takes DistrictDocument and creates Slugable out of it:
private Fizz createFizz(DistrictDocument doc) {
return new Fizz().name(doc.getName()).fizz(doc.getFizz());
}
Is there a way to put that method into my stream above so I get Map<String, List<Fizz>> ?
I tried adding 2nd argument to groupingBy but couldn't find a proper way and been always getting compilation errors.
Edit:
What if my createFizz returns List<Fizz> ? Is there an option to flat this list in Collectors.mapping becasue I still want to have Map<String, List<Fizz>> instead of Map<String, List<List<Fizz>>>

You need to chain a Collectors.mapping() collector to the Collectors.groupingBy() collector:
Map<String, List<Fizz>> collect =
docs.stream()
.collect(Collectors.groupingBy(DistrictDocument::getCity,
Collectors.mapping(d->createFizz(d),Collectors.toList())));
If createFizz(d) would return a List<Fizz, you can flatten it using Java 9's Collectors.flatMapping:
Map<String, List<Fizz>> collect =
docs.stream()
.collect(Collectors.groupingBy(DistrictDocument::getCity,
Collectors.flatMapping(d->createFizz(d).stream(),Collectors.toList())));
If you can't use Java 9, perhaps using Collectors.toMap will help:
Map<String, List<Fizz>> collect =
docs.stream()
.collect(Collectors.toMap(DistrictDocument::getCity,
d->createFizz(d),
(l1,l2)->{l1.addAll(l2);return l1;}));

In case you'd like to do a doubly nested grouping:
Let's say you have a collection of EducationData objects that contain School name, teacher name, and student name. But you want a nested map that looks like :
What you have
class EducationData {
String school;
String teacher;
String student;
// getters setters ...
}
What you want
Map<String, Map<String, List<String>>> desiredMapOfMpas ...
// which would look like this :
"East High School" : {
"Ms. Jackson" : ["Derek Shepherd", "Meredith Grey", ...],
"Mr. Teresa" : ["Eleanor Shellstrop", "Jason Mendoza", ...],
....
}
How to Get There
import static java.util.stream.Collectors.*;
public doubleNestedGroup(List<EducationData> educations) {
Map<String, Map<String, List<String>>> nestedMap = educations.stream()
.collect(
groupingBy(
EducationData::getSchool,
groupingBy(
EducationData::getTeacher,
mapping(
EducationData::getStudent,
toList()
)
)
)
);
}

Related

Java Streams filter two Lists of Maps using ifPresentOrElse()

I have two lists:
Map<String, String> map1 = new HashMap<>();
map1.put("age", "30");
map1.put("name", "john");
Map<String, String> map2 = new HashMap<>();
map1.put("age", "31");
map1.put("name", "marry");
List<Map<String, String>> list1 = new ArrayList<>();
list1.add(map1);
list1.add(map2);
Map<String, String> map3 = new HashMap<>();
map1.put("age", "40");
map1.put("name", "mike");
Map<String, String> map4 = new HashMap<>();
map1.put("age", "41");
map1.put("name", "terry");
List<Map<String, String>> list2 = new ArrayList<>();
list1.add(map3);
list1.add(map4);
I want to iterate through the list1 and find if age is found. If age is found, it should return the name.
If the target age is not found in the list1, I want to iterate through the list2, and if the result was found return the name.
I'm thinking of something along these lines:
list1.stream()
.filter(a -> a.get("age").equals("40"))
.findAny()
.ifPresentOrElse()
How can I achieve this using streams?
Use the Power of Objects
The usage of maps in your code is an example of abuse of collections. Storing the data in such a way is inconvenient and error-prone.
Instead of trying to substitute a domain object with a Map, you need to define a class (or a record).
It'll give you many advantages that you're depriving yourself by using maps:
Ability to use the proper type for every property instead of keeping everything as strings;
No need to perform parsing;
Self-explanatory method names instead of hard-coded string keys, and your code will not fail because you've misspelled a key;
The code is easier to read and maintain.
For the sake of simplicity and conciseness, I'll go with a Java 16 record:
public record Person(String name, int age) {}
And now we have two lists of Person instead of two lists of maps.
To implement the logic when we're starting with examining the first list and only if a result was not found we proceed by iterating through the second list, we can make use of the Java 9 method or() defined by Optional. While invoked on the optional object containing a value, or() returns the same optional, otherwise it'll return an optional produced by a supplier provided as an argument.
For convenience, we can define a method that takes a List<Person> and the target age and returns an optional result.
public static Optional<Person> getPersonByAge(List<Person> people, int age) {
return people.stream().filter(pers -> pers.age() == age).findFirst();
}
We can make this method reusable by making it generic. So it would expect a List<T> and a Predicate<T> as its arguments.
public static <T> Optional<T> getPersonByAge(List<T> people,
Predicate<T> predicate) {
return people.stream().filter(predicate).findFirst();
}
And that how we can apply it:
List<Person> list1 = List.of(new Person("john", 30), new Person("marry", 31));
List<Person> list2 = List.of(new Person("mike", 40), new Person("terry", 41));
Predicate<Person> age40 = pers -> pers.age() == 40;
String name = getPersonByAge(list1, age40)
.or(() -> getPersonByAge(list2, age40))
.orElseThrow()
.name();
System.out.println(name);
Output:
mike
A link to Online Demo
If you want to use the map so you can:
Optional<String> nameByAge = getNameByAge(list1, "40").or(()->getNameByAge(list2, "40"));
public static Optional<String> getNameByAge(List<Map<String, String>> people, String age) {
return people.stream().filter(person -> person.get("age").equals(age)).findAny().map(person-> person.get("name"));
}

Filter Map of Map of List

I have a HashMap of type:
Map<String, Map<String, List<MyPojo>>> myMap;
MyPojo has an element String domain.
In some cases this domain can be null.
I want to filter my map so that none of the submap Map<String, List<MyPojo>> should have null domain.
Opening note: You probably shouldn't be having a Map<String, Map<String, List<MyPojo>>> - that's far too convoluted. There should be more written-out types here. Perhaps a Map<String, Students> or some such. Your question doesn't make clear what your problem domain is, so all I can say is that your starting type is probably not good code style.
Let's get to your question:
If you mean filter as in j.u.Stream's filter, then you can't. The Map interface doesn't have a removeIf, and the stream/filter stuff isn't about changing existing types, only about making new stuff. Any attempt to modify the underlying map would just get you ConcurrentModificationExceptions.
Here is how to change the map 'in place'
var it = myMap.entrySet().iterator();
while (it.hasNext()) {
if (it.next().getValue().values().stream()
// we now have a stream of lists.
.anyMatch(
list -> list.stream().anyMatch(mp -> mp.getDomain() == null))) {
it.remove();
}
}
You have a nested anyMatch operation here: You want to remove a k/v pair if any of the entries in the submap contains a list for which any of its entries has a null domain.
Let's see it in action:
import java.util.*;
import java.util.stream.*;
#lombok.Value class MyPojo {
String domain;
}
class Test { public static void main(String[] args) {
Map<String, Map<String, List<MyPojo>>> myMap = new HashMap<>();
myMap.put("A", Map.of("X", List.of(), "Y", List.of(new MyPojo(null))));
myMap.put("B", Map.of("V", List.of(), "W", List.of(new MyPojo("domain"))));
System.out.println(myMap);
var it = myMap.entrySet().iterator();
while (it.hasNext()) {
if (it.next().getValue().values().stream()
// we now have a stream of lists.
.anyMatch(
list -> list.stream().anyMatch(mp -> mp.getDomain() == null))) {
it.remove();
}
}
System.out.println(myMap);
}}
Code that produces a new map is not that hard to figure out given the above.
As I mention in the comment, it's unclear (to me at least) if you want the mappings to stay in place for empty Lists or Maps after your filtering, but if you don't care about empty Maps/Lists afterwards you can just use:
map.values().stream().flatMap(v -> v.values().stream())
.forEach(l -> l.removeIf(p -> p.getDomain() == null));

How to convert List<Person> to Map<String, List<Double>> instead of Map<String, List<Person>>?

Considering Person class has three fields name(String), age(int), salary(double).
I want to create a map with name as key and value as salary (instead of Person object itself), if key is not unique then use linkedList to hold the values of all duplicate keys.
I am referring to the solution given in the link given below:
Idiomatically creating a multi-value Map from a Stream in Java 8
but still unclear how to create hashmap with Salary as value.
I can create map<String, List<Double>> with forEach(). code is given below:
List<Person> persons= new ArrayList<>();
persons.add(p1);
persons.add(p2);
persons.add(p3);
persons.add(p4);
Map<String, List<Double>> personsByName = new HashMap<>();
persons.forEach(person ->
personsByName.computeIfAbsent(person.getName(), key -> new LinkedList<>())
.add(person.getSalary())
);
but I am trying to use "groupingBy & collect" to create a map.
If we want Person object itself as value then the code is given below:
Map<String, List<Person>> personsByNameGroupingBy = persons.stream()
.collect(Collectors.groupingBy(Person::getName, Collectors.toList()));
But I want to create a map with salary as value like given below:
Map<String, List<Double>>
How to achieve this scenario?
You can use Collectors.mapping to map the Person instances to the corresponding salaries:
Map<String, List<Double>> salariesByNameGroupingBy =
persons.stream()
.collect(Collectors.groupingBy(Person::getName,
Collectors.mapping(Person::getSalary,
Collectors.toList())));
Create a Map and then do a forEach, for each item, check your Map if it has a specific key or not, if not, put new key and emptyList to collect your data. Check the code below.
Map<String, List<Double>> data = new HashMap<>();
persons.forEach(
p->{
String key = p.getName();
if(!data.contains(key)){
data.put(key, new ArrayList());
}
data.get(key).add(p.getSalary);
}
);

Java8 : stream and map transformations

I want to turn a List into something different.
I have a POJO class like this (I removed here getters/setters) :
public class Person {
private String personId;
private String registrationId;
}
A Person (personId) can have many registrationId, so different Person instances may refer the same real person (but have different registrationId).
I have a List<Person> persons as follows with 3 elements :
persons = [{"person1", "regis1"}, {"person1", "regis2"}, {"person2", "regis1"}];
I would like something like this (so that each key refers to a person and for each person I got the list of registrationId) :
Map<String, List<String>> personsMap = [ "person1" : {"regis1", "regis2"}, "person2" : {"regis2"}]
I know I can do something like this :
Map<String, List<Person>> map = persons.stream().collect(Collectors.groupingBy(Person::getPersonId));
But I got a Map<String,List<Person>> and I would like a Map<String,List<String>>
Any ideas ?
Thanks
Use Collectors.mapping to map the Person instances to the corresponding registrationId Strings:
Map<String, List<String>> map =
persons.stream()
.collect(Collectors.groupingBy(Person::getPersonId,
Collectors.mapping(Person::getRegistrationId,
Collectors.toList())));
And also you can use toMap with merge function
Map<String,List<String>> map= persons
.stream()
.collect(Collectors.toMap(Person::getPersonId,
value->new ArrayList<>(Arrays.asList(Person::getRegistrationId)),
(l1,l2)->{l1.addAll(l2);return l1;}));
as #Holger commented not to need create a temporary array.
Map<String,List<String>> map= persons
.stream()
.collect(Collectors.toMap(Person::getPersonId,
value->new ArrayList<>(Collections.singletonList(Person::getRegistrationId)),
(l1,l2)->{l1.addAll(l2);return l1;}));

Java Stream Collectors.toMap value is a Set

I want to use a Java Stream to run over a List of POJOs, such as the list List<A> below, and transform it into a Map Map<String, Set<String>>.
For example, class A is:
class A {
public String name;
public String property;
}
I wrote the code below that collects the values into a map Map<String, String>:
final List<A> as = new ArrayList<>();
// the list as is populated ...
// works if there are no duplicates for name
final Map<String, String> m = as.stream().collect(Collectors.toMap(x -> x.name, x -> x.property));
However, because there might be multiple POJOs with the same name, I want the value of the map be a Set. All property Strings for the same key name should go into the same set.
How can this be done?
// how do i create a stream such that all properties of the same name get into a set under the key name
final Map<String, Set<String>> m = ???
groupingBy does exactly what you want:
import static java.util.stream.Collectors.*;
...
as.stream().collect(groupingBy((x) -> x.name, mapping((x) -> x.property, toSet())));
#Nevay 's answer is definitely the right way to go by using groupingBy, but it is also achievable by toMap by adding a mergeFunction as the third parameter:
as.stream().collect(Collectors.toMap(x -> x.name,
x -> new HashSet<>(Arrays.asList(x.property)),
(x,y)->{x.addAll(y);return x;} ));
This code maps the array to a Map with a key as x.name and a value as HashSet with one value as x.property. When there is duplicate key/value, the third parameter merger function is then called to merge the two HashSet.
PS. If you use Apache Common library, you can also use their SetUtils::union as the merger
Same Same But Different
Map<String, Set<String>> m = new HashMap<>();
as.forEach(a -> {
m.computeIfAbsent(a.name, v -> new HashSet<>())
.add(a.property);
});
Also, you can use the merger function option of the Collectors.toMap function
Collectors.toMap(keyMapper,valueMapper,mergeFunction) as follows:
final Map<String, String> m = as.stream()
.collect(Collectors.toMap(
x -> x.name,
x -> x.property,
(property1, property2) -> property1+";"+property2);

Categories

Resources