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

[亡魂溺海] 提交于 2020-01-01 05:19:12

问题


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


回答1:


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



回答2:


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.




回答3:


I have not done any testing of this code, but it compiles so it must 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;
}



回答4:


It's still pretty ugly, and I think it would be improved by changing how you model things a bit, but I've only managed to come up with the following so far:

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 contained the mapping and could be keyed on would 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]}
}



回答5:


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 classical variant. So here is a solution that takes advantage of Streams without sacrificing much performance.

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


来源:https://stackoverflow.com/questions/30245142/how-to-group-elements-of-a-list-by-elements-of-another-in-java-8

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!