How do I map this in Java 8 using the stream API? - java

Ok, so I have a List<Person>. and each Person has a List<String> that is a list of phone numbers that that person owns.
So this is the basic structure:
public class Person {
private String name;
private List<String> phoneNumbers;
// constructors and accessor methods
}
I would like to create a Map<String, Person> where the key is each phone number that the person owns, and the value is the actual person.
So to explain better. If I had this List<Person>:
Person bob = new Person("Bob");
bob.getPhoneNumbers().add("555-1738");
bob.getPhoneNumbers().add("555-1218");
Person john = new Person("John");
john.getPhoneNumbers().add("518-3718");
john.getPhoneNumbers().add("518-3115");
john.getPhoneNumbers().add("519-1987");
List<Person> list = new ArrayList<>();
list.add(bob);
list.add(john);
and I invoked this method. It would give me the following Map<String, Person>
Map<String, Person> map = new HashMap<>();
map.put("555-1738", bob);
map.put("555-1218", bob);
map.put("518-3718", john);
map.put("518-3115", john);
map.put("519-1987", john);
I would like to know how to achieve this using the stream API, as I already know how I would do it using for loops and such.

If you have list of persons called persons and a class called PhoneNumberAndPerson (you could use a generic Tuple or Pair instead)
These are the steps:
For each person, take each phone number of that person. For each of those phone numbers, create a new instance of PhoneNumberAndPerson and add that to a list. Use flatMap to make one single list of all these smaller lists. To make a Map out of this list you supply one function to extract a key from a PhoneNumberAndPerson and another function to extract a Person from that same PhoneNumberAndPerson.
persons.stream()
.flatMap(person -> person.getPhoneNumbers().stream().map(phoneNumber -> new PhoneNumberAndPerson(phoneNumber, person)))
.collect(Collectors.toMap(pp -> pp.getPhoneNumber(), pp -> pp.getPerson()));

Without additional class and pre-creating map:
Map<String, Person> result = list.stream().collect(HashMap::new,
(map, p) -> p.getPhoneNumbers().forEach(phone -> map.put(phone, p)), HashMap::putAll);

Without creating other classes i'd go for something like this:
Map<String, String> map = new HashMap<>();
list.stream().forEach(p -> p.getPhoneNumbers().forEach(n -> map.put(n, p.getName())));
Edit: As suggested by Simon, you can use the collect method, but it can be tricky if you want to create a map using the class that you provided, with a simpler class ( without using List but just a plain String in order to store the number)
you can simply call the code below that returns a Map
list.stream().collect(Collectors.toMap(Person::getPhoneNumber, Person::getName));

Related

Getting multiple list of properties from a List of Objects in Java 8

Considering I have a list of objects List<Emp> where Emp has 3 properties name, id, and age. What is the fastest way to get 3 lists like List<String> names, List<String> ids, and List<Integer> ages.
The simplest I could think of is to iterate over the entire list and keep adding to these 3 lists. But, I was wondering if there is an easier way to do it with Java 8 streams?
Thanks in advance.
It's a very interesting question, however, there is no dedicated collector to handle such use case.
All you can is to use 3 iterations (Streams) respectively:
List<String> names = employees.stream().map(Emp::name).collect(Collectors.toList());
List<Integer> ids = employees.stream().map(Emp::id).collect(Collectors.toList());
List<Integer> ages = employees.stream().map(Emp::age).collect(Collectors.toList());
Edit - write the own collector: you can use the overloaded method Stream::collect(Supplier, BiConsumer, BiConsumer) to implement your own collector doing what you need:
Map<String, List<Object>> newMap = employees.stream().collect(
HashMap::new, // Supplier of the Map
(map, emp) -> { // BiConsumer accumulator
map.compute("names", remappingFunction(emp.getName()));
map.compute("ages", remappingFunction(emp.getAge()));
map.compute("ids", remappingFunction(emp.getId()));
},
(map1, map2) -> {} // BiConsumer combiner
);
Practically, all it does is extracting the wanted value (name, age...) and adding it to the List under the specific key "names", "ages" etc. using the method Map::compute that allows to compute a new value based on the existing (null by default if the key has not been used).
The remappingFunction that actually creates a new List or adds a value looks like:
private static BiFunction<String, List<Object>, List<Object>> remappingFunction(Object object) {
return (key, list) -> {
if (list == null)
list = new ArrayList<>();
list.add(object);
return list;
};
}
Java 8 Stream has some API to split the list into partition, such as:
1. Collectros.partitioningBy(..) - which create two partitions based on some Predicate and return Map<Boolean, List<>> with values;
2. Collectors.groupingBy() - which allows to group stream by some key and return resulting Map.
But, this is not really your case, since you want to put all properties of the Emp object to different Lists. I'm not sure that this can be achieved with such API, maybe with some dirty workarounds.
So, yes, the cleanest way will be to iterate through the Emp list and out all properties to the three Lists manually, as you have proposed.

Convert a List of objects to a Map using Streams

I have a list of objects of class A:
List<A> list;
class A {
String name;
String lastname;
//Getter and Setter methods
}
I want to convert this list to a map from name to a set of lastnames:
Map<String, Set<String>> map;
For example, for the following list:
John Archer, John Agate, Tom Keinanen, Tom Barren, Cindy King
The map would be:
John -> {Archer, Agate}, Tom -> {Keinanen, Barren}, Cindy -> {King}
I tried the following code, but it returns a map from name to objects of class A:
list.stream.collect(groupingBy(A::getFirstName, toSet()));
Map< String, Set<String>> map = list.stream()
.collect(
Collectors.groupingBy(
A::getFirstName, Collectors.mapping(
A::getLastName, Collectors.toSet())));
You were on the right track you need to use:
Collectors.groupingBy to group by the firstName.
And then use a downstream collector like Collectors.mappping as a second parameter of Collectors.groupingBy to map to the lastName .
And then finally collect that in a Set<String> by invoking Collectors.toSet:
You never told the collector to extract last names.
I suppose you need something like
list.stream
.collect(groupingBy(
A::getFirstName, // The key is extracted.
mapping( // Map the stream of grouped values.
A::getLastName, // Extract last names.
toSet() // Collect them into a set.
)));

convert List<E> to Map<String, List<String>> using java 8 streams

I am looking for some help in converting a List of objects to a Map<String, List<String>>.
class Person {
private String name;
private int age;
}
I have a List<Person> and I want to collect Map<int, List<String>> with key being age and value being list of names of Persons with same age.
I tried in these lines but did not work
persons.stream().collect(Collectors.groupingBy(p -> p.getAge()), );
Use this overload of groupingBy which accepts a downstream collector:
Map<Integer, List<String>> map = persons.stream()
.collect(Collectors.groupingBy(Person::getAge,
Collectors.mapping(Person::getName, Collectors.toList())));
If the purpose of this is to enable fast lookup, have you thought about using an indexed collection like Data Store:
https://github.com/jparams/data-store
You can do something like :
Store<Person> store = new MemoryStore<Person>();
store.index("name", Person::getName);
store.addAll(listOfPeople); // populate your store with data
Person personFound = person.get("name", "bob");
You can multiple indexes on the same data. You can even create case insensitive indexes etc.

Streams java 8 Object and String in Map

I got 3 classes.
Angestellte (simply contains some stuff like names etc.)
Department (only contains a String)
and ttest (for testing obviously)
I want to put all the workers "Angestellte" into their Departments. So basically the output should be:
Personalabteilung: 4
Buchhaltung: 3
Fertigung: 3
I am trying to put the Map as Map with Department and Long
but ultimately I would like to have the Map with String and Long.
I also think my Collectors.counting() doesn't work that way I put it.
I don't really know how to address my Stream of Strings after I have already mapped it. Thats why I put three ? in the code.
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class ttest {
public static void main(String[] args){
Department d1 = new Department("Personalabteilung");
Department d2 = new Department("Buchhaltung");
Department d3 = new Department("Fertigung");
List<Angestellte> AlleAng = Arrays.asList(
new Angestellte("Sandra","Bullock",d3,3450, "Female"),
new Angestellte("Yutta","Knie",d1,2800, "Female"),
new Angestellte("Ludwig","Herr",d3,3850, "Male"),
new Angestellte("Peter","Pan",d2,1850, "Male"),
new Angestellte("Nicky","Smith",d3,2100, "Female"),
new Angestellte("Herbert","Rotwein",d2,2450, "Male"),
new Angestellte("Sandra","Siech",d1,1100, "Female"),
new Angestellte("Florian","Schwarzpeter",d2,2800, "Male"),
new Angestellte("Herrietta","Glas",d1,2100, "Female"),
new Angestellte("Brock","Builder",d1,6000, "Male"));
Map<Department, Long> DepAnz = AlleAng.stream()
.map(a -> a.getDep())
.collect(Collectors.toMap(a.getDep???, Collectors.counting()));
}
}
If you want to group by department and your getter is called getDep() You can do
Map<Department, Long> DepAnz = AlleAng.stream()
.collect(Collectors.groupingBy(a -> a.getDep(), Collectors.counting()));
You need to use a group by:
Map<Department, Long> DepAnz =
AlleAng.stream()
.collect(Collectors.groupingBy(Angestellte::getDep, Collectors.counting()));
The groupingBy(classifier, downstream) collector is a collector that collects the Stream element into a Map where the keys are returned by the classifier and the values are the result of applying the downstream collector to all Stream elements having an equal key. In this case, what we want is to count the values so we use Collectors.counting() as the downstream collector.
If you want to group by the the name of the department instead, you could have, assmuming the getter is called .getName() to retrieve the name:
Map<String, Long> DepAnz =
AlleAng.stream()
.collect(Collectors.groupingBy(a -> a.getDep().getName(), Collectors.counting()));
This will return a count of all the different name of departements.
Map<String, Long> map = AlleAng.stream().collect(Collectors.toMap(a -> a.getDept().getName(), a -> 1L, (Long acc, Long newValue) -> acc + newValue));
This is such a verbose usage of lambdas to achieve what you need.
Use a toMap Collector in order to map a specific key and value based on the actual Angestellte references you have got.
Of course, the groupers functions are best suited, but this works as a generic example for map grouping by some criterion

Default return value for Collectors.toMap

If we image, we have a object called person and person looks like the follwing:
class Person {
int id;
String name;
String country
// ...
// getter/setter
}
And we have a List of Person objects and we want to "convert" it to a map. We can use the following:
Map<Long, List<Person>> collect = personList.stream().
collect(Collectors.toMap(Person::getId, p -> p));
But it is possible to return a default value for the valuemapper and change the type of the valuemapper?
I thought on something like that:
Map<Long, List<Person>> collect =
personList.stream().collect(Collectors.groupingBy(Person::getId, 0));
but with this i get the following error is not applicable for the arguments
I have a workaround but i think it's not really pretty.
Map<Long, Object> collect2 = personList.stream().
collect(Collectors.toMap(Person::getId, pe -> {
return 0;
}));
If you want to map every ID of each person to the same value, that's exactly what you need to do, although you can simplify it by writing Collectors.toMap(Person::getId, pe -> 0).

Categories