How to group list items by items 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 get a Map<Person, List<Region>> based on whether the Region contains this Person zip code? In other words, how do I group regions by people whose zip codes belong to these regions?

I did this in Java 7 the old fashioned way, but now I need to port the code to take advantage of the new features in Java 8.

Thanks,

Impeto

+7
java collections java-8
source share
5 answers

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

  persons.stream().collect(Collectors.toMap( person -> person, person -> regions.stream() .filter(region -> region.getZipCodes().contains(person.getZip())) .collect(Collectors.toList()))); 
+5
source share

The original answer makes unnecessary matching with tuples, so you see the final solution. You can 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()))); 

<h / "> If I understand well, you can 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 List<Person> you get Stream<Person> . Then you map each instance to a tuple <Person, List<Region>> , which contains all regions. From there, you collect data on a map using the toMap collector, and for each person you create a Region List that contains the zip code of that person.

For example, given 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'} => [] 

I also think that zipCodes for each Region can be Set .

+2
source share

I have not tested this code, but it compiles, so it should be right (: eyeroll :).

 public Map<Person,List<Region>> mapPeopleToRegion(List<Person> people, List<Region> regions){ final Map<Person,List<Region>> personToRegion = new HashMap<>(); people.forEach(person -> personToRegion.put( person,regions.stream().filter( region -> region.getZipCodes().contains(person.getZip())) .collect(Collectors.toList()))); return personToRegion; } 
+1
source share

It's still pretty ugly, and I think it would improve by changing the way you model things a bit, but I still managed to find the following:

 public static void main(String[] args) { Person[] people = {new Person("00001"), new Person("00002"), new Person("00005")}; Region[] regions = { new Region("Region 1", Arrays.asList("00001", "00002", "00003")), new Region("Region 2", Arrays.asList("00002", "00003", "00004")), new Region("Region 3", Arrays.asList("00001", "00002", "00005")) }; Map<Person, List<Region>> result = Stream.of(regions) .flatMap(region -> region.getZipCodes().stream() .map(zip -> new SimpleEntry<>(zip, region))) .flatMap(entry -> Stream.of(people) .filter(person -> person.getZip().equals(entry.getKey())) .map(person -> new SimpleEntry<>(person, entry.getValue()))) .collect(Collectors.groupingBy(Entry::getKey, Collectors.mapping(Entry::getValue, Collectors.toList()))); result.entrySet().forEach(entry -> System.out.printf("[%s]: {%s}\n", entry.getKey(), entry.getValue())); // Output: // [Person: 0]: {[name: Region 1, name: Region 3]} // [Person: 1]: {[name: Region 1, name: Region 2, name: Region 3]} // [Person: 2]: {[name: Region 3]} } 

Having a ZipCode class that contains a mapping and can be put into action will make things cleaner:

 public static void main(String[] args) { Region r1 = new Region("Region 1"); Region r2 = new Region("Region 2"); Region r3 = new Region("Region 3"); ZipCode zipCode1 = new ZipCode("00001", Arrays.asList(r1, r3)); ZipCode zipCode2 = new ZipCode("00002", Arrays.asList(r1, r2, r3)); ZipCode zipCode3 = new ZipCode("00003", Arrays.asList()); ZipCode zipCode4 = new ZipCode("00004", Arrays.asList()); ZipCode zipCode5 = new ZipCode("00005", Arrays.asList(r3)); Person[] people = { new Person(zipCode1), new Person(zipCode2), new Person(zipCode5) }; Map<Person, List<Region>> result = Stream.of(people) .collect(Collectors.toMap(person -> person, person -> person.getZip().getRegions())); result.entrySet().forEach(entry -> System.out.printf("[%s]: {%s}\n", entry.getKey(), entry.getValue())); // Output: // [Person: 0]: {[name: Region 1, name: Region 3]} // [Person: 1]: {[name: Region 1, name: Region 2, name: Region 3]} // [Person: 2]: {[name: Region 3]} } 
0
source share

Some of the other answers contain code that does a lot of linear searching through lists. I think the Java 8 Stream solution should not be much slower than the classic version. So, here is a solution that uses threads without sacrificing great efficiency.

 List<Person> people = ... List<Region> regions = ... Map<String, List<Region>> zipToRegions = regions.stream().collect( () -> new HashMap<>(), (map, region) -> { for(String zipCode: region.getZipCodes()) { List<Region> list = map.get(zipCode); if(list == null) list = new ArrayList<>(); list.add(region); map.put(zipCode, list); } }, (m1, m2) -> m1.putAll(m2) ); Map<Person, List<Region>> personToRegions = people.stream().collect( Collectors.toMap(person -> person, person -> zipToRegions.get(person.getZip())) ); 
0
source share

All Articles