generate the Map from Lists using streams - java

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

Related

Java group a map by value where value is a List

I have a
Map<String,List<User>>map = new HashMap<>();
map.put("projectA",Arrays.asList(new User(1,"Bob"),new User(2,"John"),new User(3,"Mo")));
map.put("projectB",Arrays.asList(new User(2,"John"),new User(3,"Mo")));
map.put("projectC",Arrays.asList(new User(3,"Mo")));
Can use String instead of User.
String is a project Name but the same users can relate to different projects.
I would like to get sth like Map<User, List<String>> where
the key will represent a distinct user and a value as a list of projects' names to which he/she relates.
Bob = [projectA]
John = [projectA, projectB]
Mo = [projectA, projectB, projectC]
TQ in advance for any piece of advice.
Just loop over the map's entries and the List inside of them:
public static void main(String[] args) {
Map<String, List<User>> map = new HashMap<>();
map.put("projectA", Arrays.asList(new User(1,"Bob"),new User(2,"John"),new User(3,"Mo")));
map.put("projectB",Arrays.asList(new User(2,"John"),new User(3,"Mo")));
map.put("projectC",Arrays.asList(new User(3,"Mo")));
Map<User, List<String>> result = new HashMap<>();
for(Map.Entry<String, List<User>> e:map.entrySet()) {
for(User u:e.getValue()) {
result.putIfAbsent(u, new ArrayList<>());
result.get(u).add(e.getKey());
}
}
System.out.println(result);
}
public static record User(int id, String name) {}
prints
{User[id=1, name=Bob]=[projectA], User[id=2, name=John]=[projectB, projectA], User[id=3, name=Mo]=[projectB, projectA, projectC]}
To reverse this Map, you need to iterate over its entries and for each distinct user create an entry containing a list of projects as a value in the resulting Map.
Java 8 computeIfAbsent()
This logic can be implemented using Java 8 methods Map.computeIfAbsent() and Map.forEach().
Map<String, List<User>> usersByProject = // initilizing the source map
Map<User, List<String>> projectsByUser = new HashMap<>();
usersByProject.forEach((project, users) ->
users.forEach(user -> projectsByUser.computeIfAbsent(user, k -> new ArrayList<>())
.add(project))
);
Stream API
Stream-based implementation would require a bit more effort.
The core logic remains the same. But there's one important peculiarity: we would need to generate from each entry of the source Map a sequence of new elements, containing references to a particular user and a project.
To carry this data we would need an auxiliary type, and a Java 16 record fits into this role very well. And quick and dirty alternative would be to use Map.Entry, but it's better to avoid resorting to this option because methods getKey()/getValue() are faceless, and it requires more effort to reason about the code. You can also define a regular class if you're using an earlier version of JDK.
public record UserProject(User user, String project) {}
That's how a stream-based solution might look like:
Map<String, List<User>> usersByProject = Map.of(
"projectA", List.of(new User(1, "Bob"), new User(2, "John"), new User(3, "Mo")),
"projectB", List.of(new User(2, "John"), new User(3, "Mo")),
"projectC", List.of(new User(3, "Mo"))
);
Map<User, List<String>> projectByUsers = usersByProject.entrySet().stream()
.flatMap(entry -> entry.getValue().stream().
map(user -> new UserProject(user, entry.getKey()))
)
.collect(Collectors.groupingBy(
UserProject::user,
Collectors.mapping(UserProject::project,
Collectors.toList())
));
projectsByUser.forEach((k, v) -> System.out.println(k + " -> " + v));
Output:
User[id=1, name=Bob] -> [projectA]
User[id=2, name=John] -> [projectA, projectB]
User[id=3, name=Mo] -> [projectA, projectC, projectB]

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 calculate multiple sum in an object grouping by a property in Java8 stream?

I'm doing some calculations with some data. The object has several numeric properties. I have achieved calculating grouping by the type on one property. But the need is to calculate on each numeric value. So, is there any method to solve this problem?
Person p1 = new Person("p1","type1",18,3);
Person p2 = new Person("p2","type2",10,6);
Person p3 = new Person("p3","type1",5,8);
List<Person> personList = new ArrayList<>();
personList.add(p1);
personList.add(p2);
personList.add(p3);
Map<String, Integer> collect =
personList.stream().collect(
Collectors.groupingBy(
Person::getType,Collectors.summingInt(Person::getAge)
)
);
I expect the sum of each property and a single result map. The value of the map is a map/array/list.
For example:
type1 [23,11]
type2 [10,6]
As pointed out in comments as well, defining the type of your output matters here. To add a gist of how you can plan to do it, you can use Collectors.toMap with custom mergeFunction as :
List<Person> personList = new ArrayList<>();
Map<String, Person> collect = personList.stream()
.collect(Collectors.toMap(Person::getType, a -> a, (p1, p2) ->
new Person(p1.getX(), p1.getType(),
p1.getAge() + p2.getAge(),
p1.getCode() + p2.getCode()))); // named the other attribute 'code'
Now, against every type you have a Person object with integer values summed.

Use java stream to group by 2 keys on the same type

Using java stream, how to create a Map from a List to index by 2 keys on the same class?
I give here a code Example, I would like the map "personByName" to get all person by firstName OR lastName, so I would like to get the 3 "steves": when it's their firstName or lastname. I don't know how to mix the 2 Collectors.groupingBy.
public static class Person {
final String firstName;
final String lastName;
protected Person(String firstName, String lastName) {
super();
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
}
#Test
public void testStream() {
List<Person> persons = Arrays.asList(
new Person("Bill", "Gates"),
new Person("Bill", "Steve"),
new Person("Steve", "Jobs"),
new Person("Steve", "Wozniac"));
Map<String, Set<Person>> personByFirstName = persons.stream().collect(Collectors.groupingBy(Person::getFirstName, Collectors.toSet()));
Map<String, Set<Person>> personByLastName = persons.stream().collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()));
Map<String, Set<Person>> personByName = persons.stream().collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()));// This is wrong, I want bot first and last name
Assert.assertEquals("we should search by firstName AND lastName", 3, personByName.get("Steve").size()); // This fails
}
I found a workaround by looping on the 2 maps, but it is not stream-oriented.
You can do it like this:
Map<String, Set<Person>> personByName = persons.stream()
.flatMap(p -> Stream.of(new SimpleEntry<>(p.getFirstName(), p),
new SimpleEntry<>(p.getLastName(), p)))
.collect(Collectors.groupingBy(SimpleEntry::getKey,
Collectors.mapping(SimpleEntry::getValue, Collectors.toSet())));
Assuming you add a toString() method to the Person class, you can then see result using:
List<Person> persons = Arrays.asList(
new Person("Bill", "Gates"),
new Person("Bill", "Steve"),
new Person("Steve", "Jobs"),
new Person("Steve", "Wozniac"));
// code above here
personByName.entrySet().forEach(System.out::println);
Output
Steve=[Steve Wozniac, Bill Steve, Steve Jobs]
Jobs=[Steve Jobs]
Bill=[Bill Steve, Bill Gates]
Wozniac=[Steve Wozniac]
Gates=[Bill Gates]
You could merge the two Map<String, Set<Person>> for example
Map<String, Set<Person>> personByFirstName =
persons.stream()
.collect(Collectors.groupingBy(
Person::getFirstName,
Collectors.toCollection(HashSet::new))
);
persons.stream()
.collect(Collectors.groupingBy(Person::getLastName, Collectors.toSet()))
.forEach((str, set) -> personByFirstName.merge(str, set, (s1, s2) -> {
s1.addAll(s2);
return s1;
}));
// personByFirstName contains now all personByName
One way would be by using the newest JDK12's Collector.teeing:
Map<String, List<Person>> result = persons.stream()
.collect(Collectors.teeing(
Collectors.groupingBy(Person::getFirstName,
Collectors.toCollection(ArrayList::new)),
Collectors.groupingBy(Person::getLastName),
(byFirst, byLast) -> {
byLast.forEach((last, peopleList) ->
byFirst.computeIfAbsent(last, k -> new ArrayList<>())
.addAll(peopleList));
return byFirst;
}));
Collectors.teeing collects to two separate collectors and then merges the results into a final value. From the docs:
Returns a Collector that is a composite of two downstream collectors. Every element passed to the resulting collector is processed by both downstream collectors, then their results are merged using the specified merge function into the final result.
So, the above code collects to a map by first name and also to a map by last name and then merges both maps into a final map by iterating the byLast map and merging each one of its entries into the byFirst map by means of the Map.computeIfAbsent method. Finally, the byFirst map is returned.
Note that I've collected to a Map<String, List<Person>> instead of to a Map<String, Set<Person>> to keep the example simple. If you actually need a map of sets, you could do it as follows:
Map<String, Set<Person>> result = persons.stream().
.collect(Collectors.teeing(
Collectors.groupingBy(Person::getFirstName,
Collectors.toCollection(LinkedHashSet::new)),
Collectors.groupingBy(Person::getLastName, Collectors.toSet()),
(byFirst, byLast) -> {
byLast.forEach((last, peopleSet) ->
byFirst.computeIfAbsent(last, k -> new LinkedHashSet<>())
.addAll(peopleSet));
return byFirst;
}));
Keep in mind that if you need to have Set<Person> as the values of the maps, the Person class must implement the hashCode and equals methods consistently.
If you want a real stream-oriented solution, make sure you don't produce any large intermediate collections, else most of the sense of streams is lost.
If just you want to just filter all Steves, filter first, collect later:
persons.stream
.filter(p -> p.getFirstName().equals('Steve') || p.getLastName.equals('Steve'))
.collect(toList());
If you want to do complex things with a stream element, e.g. put an element into multiple collections, or in a map under several keys, just consume a stream using forEach, and write inside it whatever handling logic you want.
You cannot key your maps by multiple values. For what you want to achieve, you have three options:
Combine your "personByFirstName" and "personByLastName" maps, you will have duplicate values (eg. Bill Gates will be in the map under the key Bill and also in the map under the key Gates). #Andreas answer gives a good stream-based way to do this.
Use an indexing library like lucene and index all your Person objects by first name and last name.
The stream approach - it will not be performant on large data sets but you can stream your collection and use filter to get your matches:
persons
.stream()
.filter(p -> p.getFirstName().equals("Steve")
|| p.getLastName().equals("Steve"))
.collect(Collectors.asList());
(I've written the syntax from memory so you might have to tweak it).
If I got it right you want to map each Person twice, once for the first name and once for the last.
To do this you have to double your stream somehow. Assuming Couple is some existing 2-tuple (Guava or Vavr have some nice implementation) you could:
persons.stream()
.map(p -> new Couple(new Couple(p.firstName, p), new Couple(p.lastName, p)))
.flatMap(c -> Stream.of(c.left, c.right)) // Stream of Couple(String, Person)
.map(c -> new Couple(c.left, Arrays.asList(c.right)))
.collect(Collectors.toMap(Couple::getLeft, Couple::getRight, Collection::addAll));
I didn't test it, but the concept is: make a stream of (name, person), (surname, person)... for every person, then simply map for the left value of each couple. The asList is to have a collection as value. If you need a Set chenge the last line with .collect(Collectors.toMap(Couple::getLeft, c -> new HashSet(c.getRight), Collection::addAll))
Try SetMultimap, either from Google Guava or my library abacus-common
SetMultimap<String, Person> result = Multimaps.newSetMultimap(new HashMap<>(), () -> new HashSet<>()); // by Google Guava.
// Or result = N.newSetMultimap(); // By Abacus-Util
persons.forEach(p -> {
result.put(p.getFirstName(), p);
result.put(p.getLastName(), p);
});

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

Categories

Resources