Java8 : stream and map transformations - java

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;}));

Related

How to preserve all Subgroups while applying nested groupingBy collector

I am trying to group a list of employees by the gender and department.
How do I ensure all departments are included in a sorted order for each gender, even when the relevant gender count is zero?
Currently, I have the following code and output
employeeRepository.findAll().stream()
.collect(Collectors.groupingBy(Employee::getGender,
Collectors.groupingBy(Employee::getDepartment,
Collectors.counting())));
//output
//{MALE={HR=1, IT=1}, FEMALE={MGMT=1}}
Preferred output is:
{MALE={HR=1, IT=1, MGMT=0}, FEMALE={HR=0, IT=0, MGMT=1}}
To achieve that, first you have to group by department, and only then by gender, not the opposite.
The first collector groupingBy(Employee::getDepartment, _downstream_ ) will split the data set into groups based on department. As it downstream collector partitioningBy(employee -> employee.getGender() == Employee.Gender.MALE, _downstream_ ) will be applied, it'll divide the data mapped to each department into two parts based on the employee gender. And finally, Collectors.counting() applied as a downstream will provide the total number of employees of each gender for every department.
So the intermediate map produced by the collect() operation will be of type Map<String, Map<Boolean, Long>> - employee count by gender (Boolean) for each department (for simplicity, department is a plain string).
The next step in transform this map into Map<Employee.Gender, Map<String, Long>> - employee count by department for each gender.
My approach is to create a stream over the entry set and replace each entry with a new one, which will hold a gender as its key and in order to preserve the information about a department its value in turn will be an entry with a department as a key and a with a count by department as its value.
Then collect the stream of entries with groupingBy by the entry key. Apply mapping as a downstream collector to extract the nested entry. And then apply Collectors.toMap() to collect entries of type Map.Entry<String, Long> into map.
all departments are included in a sorted order
To insure the order in the nested map (department by count) a NavigableMap should be used.
In order to do that, a flavor of toMap() that expects a mapFactory needs to be used (it also expects a mergeFunction which isn't really useful for this task since there will be no duplicates, but it has to be provided as well).
public static void main(String[] args) {
List<Employee> employeeRepository =
List.of(new Employee("IT", Employee.Gender.MALE),
new Employee("HR", Employee.Gender.MALE),
new Employee("MGMT", Employee.Gender.FEMALE));
Map<Employee.Gender, NavigableMap<String, Long>> departmentCountByGender = employeeRepository
.stream()
.collect(Collectors.groupingBy(Employee::getDepartment, // Map<String, Map<Boolean, Long>> - department to *employee count* by gender
Collectors.partitioningBy(employee -> employee.getGender() == Employee.Gender.MALE,
Collectors.counting())))
.entrySet().stream()
.flatMap(entryDep -> entryDep.getValue().entrySet().stream()
.map(entryGen -> Map.entry(entryGen.getKey() ? Employee.Gender.MALE : Employee.Gender.FEMALE,
Map.entry(entryDep.getKey(), entryGen.getValue()))))
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue,
Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> v1,
TreeMap::new))));
System.out.println(departmentCountByGender);
}
Dummy Employee class used for demo-purposes:
class Employee {
enum Gender {FEMALE, MALE};
private String department;
private Gender gender;
// etc.
// constructor, getters
}
Output
{FEMALE={HR=0, IT=0, MGMT=1}, MALE={HR=1, IT=1, MGMT=0}}
You can continue to work on the result of your code:
List<String> deptList = employees.stream().map(Employee::getDepartment).sorted().toList();
Map<Gender, Map<String, Long>> tmpResult = employees.stream()
.collect(Collectors.groupingBy(Employee::getGender, Collectors.groupingBy(Employee::getDepartment, Collectors.counting())));
Map<Gender, Map<String, Long>> finalResult = new HashMap<>();
for (Map.Entry<Gender, Map<String, Long>> entry : tmpResult.entrySet()) {
Map<String, Long> val = new LinkedHashMap<>();
for (String dept : deptList) {
val.put(dept, entry.getValue().getOrDefault(dept, 0L));
}
finalResult.put(entry.getKey(), val);
}
System.out.print(finalResult);
Probably readability or maintainability of code won't be good if you want to achieve result with one line of code.
However, there is one alternative if you don't mind to use third-party library: abacus-common
Map<Gender, Map<String, Integer>> result = Stream.of(employees)
.groupByToEntry(Employee::getGender, MoreCollectors.countingIntBy(Employee::getDepartment)) // step 1) group by gender
.mapValue(it -> Maps.newMap(deptList, Fn.identity(), dept -> it.getOrDefault(dept, 0), IntFunctions.ofLinkedHashMap())) // step 2) process the value.
.toMap();
Declaration: I'm the developer of abacus-common

generate the Map from Lists using streams

I have two List objects from which i want to create the Map object using java8.
List<Person> personList =..; //pid,personName,personAge;
List<Address> addressList =..;//pid,location,homeAddress;
HashMap<String, String> map = new HashMap<>();
I want to iterate both the list and generate the map by storing pid from personList and location from addressList as key and value pairs using streams.
personList.stream().filter(..)//how to iterate the second List?
Below is the sample map object it should look
map.put(pid,location);
Note: Both personList and addressList are not of same size.
Stream API does not iterate on collections; it rather creates a stream from the source, and you cannot source one stream from two different resources.
You can use a circumventing approach, and this will do what you're looking for:
List<Person> personList = ...; //pid, personName, personAge;
List<Address> addressList = ...;//pid, location, homeAddress;
HashMap<String, String> map = new HashMap<>();
//get the size of smaller list, as lists are of different sizes.
int size = Math.min(personList.size(), addressList.size());
IntStream.range(0, size)
.mapToObj(e -> new SimpleEntry<>(personList.get(e).getPid(), addressList.get(e).getLocation()))
.forEach(e -> map.put(e.getKey(), e.getValue()));
Notes:
Make sure you provide appropriate getters in Person and Address;
import java.util.AbstractMap.SimpleEntry;
It seems that two steps are needed here:
Build a map <pid, location> on the basis of addressList
Iterate through personList and use person.pid to map to the address.
In general case, a person may have several addresses which also may be duplicated, so final map may look as <pid, Set<location>>.
// 1. map pid to set of addresses
Map<String, Set<String>> addresses = addressList
.stream()
.collect(Collectors.groupingBy(
Address::getPid,
Collectors.mapping(Address::getLocation, Collectors.toSet())
));
// 2. map person ids to addresses
Map<String, Set<String>> personAddresses = personList
.stream()
.collect(Collectors.toMap(
Person::getPid,
person -> addresses.get(person.getPid()), // Set<location>
(addr1, addr2) -> addr1 // resolve possible conflicts
));
If it is guaranteed that the pid/addresses pairs are unique and map <pid, location> can be created, the above code may be simplified:
// 1. map pid to single address
Map<String, String> addressMap = addressList
.stream()
.collect(Collectors.toMap(
Address::getPid,
Address::getLocation
));
// 2. map person ids to addresses
Map<String, String> personAddress = personList
.stream()
.collect(Collectors.toMap(
Person::getPid,
// single location, provide default value
person -> addressMap.getOrDefault(person.getPid(), "Unknown address")
));
I would recommend you do it as follows:
first stream the addressList to create an address map which has the pid as the key and the address as the value.
then stream the personList, mapping to the id and filtering on the existence of that id in the address map.
This has two advantages.
The contents of personList and addressList may be in any order.
Only those ids in personList will be used, ignoring any other ids in the addressList. So the lists are not required to be one-to-one or even the same size.
First, I used two records (classes would also work exactly the same way) to house some data. For this demo, only the pertinent information is included.
record Person(String getPid) {
};
record Address(String getPid, String getLocation) {
};
Next, I create lists with the test data. Both lists will contain id's not in the other.
List<Person> personList = List.of(new Person("id1"),
new Person("id2"), new Person("id3"), new Person("id6"));
List<Address> addressList =
List.of(new Address("id3", "Utah"),
new Address("id1", "Idaho"),
new Address("id2", "Virginia"),
new Address("id5", "New York"));
Now create a map of pid to address. If two addresses exist for the same id, only the first encountered is used.
Map<String, Address> addressMap =
addressList.stream().collect(Collectors
.toMap(Address::getPid, address -> address,
(ad1,ad2)->ad1));
Now create the final map.
stream the personList and pull out the ids
now filter to ensure the given id is in the address map.
and enter the id and the location for given id in the map
Map<String, String> result = personList.stream()
.map(Person::getPid).filter(addressMap::containsKey)
.collect(Collectors.toMap(id -> id,
id -> addressMap.get(id).getLocation));
result.entrySet().forEach(System.out::println);
For the test data, the following is printed.
id2=Virginia
id1=Idaho
id3=Utah

How to construct a Map using with object properties as key using java stream and toMap or flatMap

I have list of objects and need to create a Map have key is combination of two of the properties in that object. How to achieve it in Java 8.
public class PersonDTOFun {
/**
* #param args
*/
public static void main(String[] args) {
List<PersonDTO> personDtoList = new ArrayList<>();
PersonDTO ob1 = new PersonDTO();
ob1.setStateCd("CT");
ob1.setStateNbr("8000");
personDtoList.add(ob1);
PersonDTO ob2 = new PersonDTO();
ob2.setStateCd("CT");
ob2.setStateNbr("8001");
personDtoList.add(ob2);
PersonDTO ob3 = new PersonDTO();
ob3.setStateCd("CT");
ob3.setStateNbr("8002");
personDtoList.add(ob3);
Map<String,PersonDTO> personMap = new HashMap<>();
//personMap should contain
Map<String, PersonDTO> personMap = personDtoList.stream().collect(Collectors.toMap(PersonDTO::getStateCd,
Function.identity(),(v1,v2)-> v2));
}
}
In the above code want to construct personMap with key as StateCd+StateNbr and value as PersonDTO. As existing stream and toMap function only support single argument function as key can't able to create a key as StateCd+StateNbr.
Try it like this.
The key argument to map is the key and a concatenation of the values you described.
The value is simply the object
Map<String, PersonDTO> personMap =
personDtoList
.stream()
.collect(Collectors.toMap(p->p.getStateCd() + p.getStateNbr(), p->p));
If you believe you will have duplicate keys, then you have several choices include a merge function.
the one shown below preserves the value for the first key (existing) encountered.
Map<String, PersonDTO> personMap =
personDtoList
.stream()
.collect(Collectors.toMap(p->p.getStateCd() + p.getStateNbr(), p->p,
(existingValue, lastestValue)-> existingValue));
the next one saves all instances of PersonDTO and puts same key values in a list.
Map<String, List<PersonDTO>> personMap =
personDtoList
.stream()
.collect(Collectors.groupingBy(p->p.getStateCd() +
p.getStateNbr()));
as the two answers above, you can use the toMap function that gets 2 functions as input, the first one is the way to get a unique key, the second is how to get the data.
We prefer to hold the unique key in the class its self and to call the function for the key directly from that class using the Class::Method as the function for the keys, it makes the code much more readable instead of using nested lambda functions
so using WJS example:
Map<String, PersonDTO> personMap =
personDtoList
.stream()
.collect(Collectors.toMap(p->p.getStateCd() + p.getStateNbr(), p->p));
Map<String, PersonDTO> personMap =
personDtoList
.stream()
.collect(Collectors.toMap(PersonDTO::getUniqueKey, p->p));
Note that this is exactly the same as the above logic wise
I have fixed with following implementation.
Function<PersonDTO, String> keyFun = person -> person.getStateCd()+person.getStateNbr();
Map<String, PersonDTO> personMap = personDtoList.stream().collect(Collectors.toMap(keyFun,
Function.identity(),(v1,v2)-> v2));
This will work in case while creating dynamic key if we encounter duplicate key.

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);
}
);

Grouping by and map value

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()
)
)
)
);
}

Categories

Resources