Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to group elements of a List by elements of another in Java 8

I have the following problem: Given these classes,

class Person {
    private String zip;
    ...
    public String getZip(){
        return zip;
    }
}

class Region {
    private List<String> zipCodes;
    ...
    public List<String> getZipCodes() {
        return zipCodes;
    }
}

using the Java 8 Stream API, how do I obtain a Map<Person, List<Region>> based on whether the Region contains that Person's zip code? In other words how do I group the regions by the people whose zip codes belong to those regions?

I've done it in Java 7 the old fashioned way, but now I have to migrate the code to take advantage of the new features in Java 8.

Thank you,

Impeto

like image 221
impeto Avatar asked May 14 '15 18:05

impeto


People also ask

How do you add all elements of a List to another List in Java?

Create a List by passing another list as a constructor argument. List<String> copyOflist = new ArrayList<>(list); Create a List and use addAll method to add all the elements of the source list.

What is the use of groupingBy in Java 8?

The groupingBy() method of Collectors class in Java are used for grouping objects by some property and storing results in a Map instance.

What is a flatMap in Java 8?

In Java 8 Streams, the flatMap() method applies operation as a mapper function and provides a stream of element values. It means that in each iteration of each element the map() method creates a separate new stream. By using the flattening mechanism, it merges all streams into a single resultant stream.

Can you add lists together in Java?

The addAll() method to merge two lists The addAll() method is the simplest and most common way to merge two lists.


2 Answers

I suspect the cleanest way to do this -- I'm not quite happy with the other answers posted -- would be

 persons.stream().collect(Collectors.toMap(
    person -> person,
    person -> regions.stream()
       .filter(region -> region.getZipCodes().contains(person.getZip()))
       .collect(Collectors.toList())));
like image 167
Louis Wasserman Avatar answered Oct 09 '22 16:10

Louis Wasserman


The original answer does an unnecessary mapping with tuples, so you see there the final solution. You could remove the mapping, and simply filter directly the regions list:

//A Set<Region> is more appropriate, IMO
.stream()
.collect(toMap(p -> p, 
               p -> regions.stream()
                           .filter(r -> r.getZipCodes().contains(p.getZip()))
                           .collect(toSet())));


If I understand well, you could do something like this:
import java.util.AbstractMap.SimpleEntry;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toList;

...

List<Person> persons = ...;
List<Region> regions = ...;

Map<Person, List<Region>> map = 
    persons.stream()
           .map(p -> new SimpleEntry<>(p, regions))
           .collect(toMap(SimpleEntry::getKey, 
                          e -> e.getValue().stream()
                                           .filter(r -> r.getZipCodes().contains(e.getKey().getZip()))
                                           .collect(toList())));

From the List<Person> you get a Stream<Person>. Then you map each instance to a tuple <Person, List<Region>> that contains all the regions. From there, you collect the data in a map with the toMap collector and, for each person, you build a List of Region that contains the zip code of that person.

For example, given the input:

List<Person> persons = Arrays.asList(new Person("A"), new Person("B"), new Person("C"));

List<Region> regions = 
     Arrays.asList(new Region(Arrays.asList("A", "B")), new Region(Arrays.asList("A")));

It outputs:

Person{zip='A'} => [Region{zipCodes=[A, B]}, Region{zipCodes=[A]}]
Person{zip='B'} => [Region{zipCodes=[A, B]}]
Person{zip='C'} => []

Also I guess the zipCodes for each Region could be a Set.

like image 45
Alexis C. Avatar answered Oct 09 '22 17:10

Alexis C.