Collect and map in Java8 Streams [duplicate] - java

I have the following class.
class Person {
String name;
LocalDate birthday;
Sex gender;
String emailAddress;
public int getAge() {
return birthday.until(IsoChronology.INSTANCE.dateNow()).getYears();
}
public String getName() {
return name;
}
}
I'd like to be able to group by age and then collect the list of the persons names rather than the Person object itself; all in a single nice lamba expression.
To simplify all of this I am linking my current solution that store the result of the grouping by age and then iterates over it to collect the names.
ArrayList<OtherPerson> members = new ArrayList<>();
members.add(new OtherPerson("Fred", IsoChronology.INSTANCE.date(1980, 6, 20), OtherPerson.Sex.MALE, "fred#example.com"));
members.add(new OtherPerson("Jane", IsoChronology.INSTANCE.date(1990, 7, 15), OtherPerson.Sex.FEMALE, "jane#example.com"));
members.add(new OtherPerson("Mark", IsoChronology.INSTANCE.date(1990, 7, 15), OtherPerson.Sex.MALE, "mark#example.com"));
members.add(new OtherPerson("George", IsoChronology.INSTANCE.date(1991, 8, 13), OtherPerson.Sex.MALE, "george#example.com"));
members.add(new OtherPerson("Bob", IsoChronology.INSTANCE.date(2000, 9, 12), OtherPerson.Sex.MALE, "bob#example.com"));
Map<Integer, List<Person>> collect = members.stream().collect(groupingBy(Person::getAge));
Map<Integer, List<String>> result = new HashMap<>();
collect.keySet().forEach(key -> {
result.put(key, collect.get(key).stream().map(Person::getName).collect(toList()));
});
Current solution
Not ideal and for the sake of learning I'd like to have a more elegant and performing solution.

When grouping a Stream with Collectors.groupingBy, you can specify a reduction operation on the values with a custom Collector. Here, we need to use Collectors.mapping, which takes a function (what the mapping is) and a collector (how to collect the mapped values). In this case the mapping is Person::getName, i.e. a method reference that returns the name of the Person, and we collect that into a List.
Map<Integer, List<String>> collect =
members.stream()
.collect(Collectors.groupingBy(
Person::getAge,
Collectors.mapping(Person::getName, Collectors.toList()))
);

You can use a mapping Collector to map the list of Person to a list of person names :
Map<Integer, List<String>> collect =
members.stream()
.collect(Collectors.groupingBy(Person::getAge,
Collectors.mapping(Person::getName, Collectors.toList())));

You can also use Collectors.toMap and provide mapping for key, value and merge function(if any).
Map<Integer, String> ageNameMap =
members.stream()
.collect(Collectors.toMap(
person -> person.getAge(),
person -> person.getName(), (pName1, pName2) -> pName1+"|"+pName2)
);

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

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

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

Group by a Collection attribute using Java Streams

I have an object that contains a Collection of strings, let's say the languages that a person speaks.
public class Person {
private String name;
private int age;
private List<String> languagesSpoken;
// ...
}
Now, creating some instances like this...
Person p1 = new Person("Bob", 21, Arrays.asList("English", "French", "German"));
Person p2 = new Person("Alice", 33, Arrays.asList("English", "Chinese", "Spanish"));
Person p3 = new Person("Joe", 43, Arrays.asList("English", "Dutch", "Spanish", "German"));
//put them in list
List<Person> people = Arrays.asList(p1,p2,p3);
... what I want to have is a Map<String, List<Person>>, for every language listing the persons that speak the language:
["English" -> [p1, p2, p3],
"German" -> [p1, p3],
etc. ]
Of course this can be programmed easily in an imperative way, but how to do it the functional way with Java Streams? I have tried something like people.stream.collect(groupingBy(Person::getLanguagesSpoken)) but that of course gives me a Map<List<String>, List<Person>>. All the examples I could find, are using groupingBy on Primitives or Strings.
You can break the Person instances into pairs of language and Person (using flatMap) and then group them as required:
Map<String, List<Person>> langPersons =
people.stream()
.flatMap(p -> p.getLanguagesSpoken()
.stream()
.map(l -> new SimpleEntry<>(l,p)))
.collect(Collectors.groupingBy(Map.Entry::getKey,
Collectors.mapping(Map.Entry::getValue,
Collectors.toList())));
This is possible to do without streams too, still using java-8 new features.
people.forEach(x -> {
x.getLanguagesSpoken().forEach(lang -> {
langPersons.computeIfAbsent(lang, ignoreMe -> new ArrayList<>()).add(x);
});
});

How to group some object based on id?

I've got some object with their id. I wish to group objects based on id. I mean, object with the same id should be in a group. Is there any idea?
for example:
id Name
1 Allah
1 Mohammad
2 Ali
2 Fatemeh
2 Hassan
3 hossein
3 Mahdi
3 Reza
Assuming you have this type:
class Person {
final int id;
final String name;
Person(int id, String name) {
this.id = id;
this.name = name;
}
}
You can then write the following:
List<Person> people = Arrays.asList(new Person(1, "Allah"), new Person(1, "Mohammad"), ..);
Map<Object, List<Person>> peoplePerId =
people.stream()
.collect(Collectors.groupingBy(p -> p.id));
Or perhaps, even better:
Map<Integer, List<String>> namesPerId =
people.stream()
.collect(Collectors.groupingBy(
p -> p.id,
Collectors.mapping(p -> p.name, Collectors.toList())
));
A pre-Java 8 version:
Map<Integer, List<String>> namesPerId = new HashMap<>();
for (Person person : people) {
List<String> list = namesPerId.get(person.id);
if (list == null) {
list = new ArrayList<>();
namesPerId.put(person.id, list);
}
list.add(person.name);
}
The simplest way to do it is using a Guava Multimap:
Multimaps.index(myObjects, new Function<MyObject, Integer>() {
public Integer apply(MyObject input) {
return input.getId();
}
});
This will result in a multimap which has your IDs as single keys and the values as your objects.
In Java8 you can write this even simpler with a lambda expression:
Multimaps.index(myObjects, o -> o.getId());
If you cannot use Guava then you need to implement this behavior yourself as shown in the answer of Lukas Eder.
As for Guava, this is a quite popular library developed by Google which contains a ton of common utility classes, methods and advanced collection classes (such as bidirectional maps, multisets, and multimaps).
For more information, have a look at the Guava Documentation.
If you are not reluctant to use a third-party library, you can use Google's Guava library. There exists a MultiMap that maps a key to a list of values, see here. It works slightly different than a Map<Integer, List<Person>>. It has some built-in features like values() that returns all values in all lists as a flat collection. Which variant suits better depends on what you want to do with the map.

Categories

Resources