如何在Java 8中按另一个列表的元素对列表元素进行分组

8
我可以帮你进行翻译。以下是需要翻译的内容:

我有一个问题:给定以下类,

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

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

使用Java 8的Stream API,我如何根据Region是否包含该Person的邮编来获取一个Map<Person, List<Region>>呢?换句话说,我如何将那些属于某些地区的人的邮编分组到这些地区中去?我已经用Java 7老式方法做过了,但现在我必须迁移代码以利用Java 8的新特性。
谢谢,
Impeto

如果一个人只有一个邮政编码,那么他怎么可能属于多个地区呢?一个邮政编码能够属于多个地区吗? - Eran
@Eran 是的,区域是带有名称的邮政编码列表。它们可以重叠。 - impeto
让我更好地说明一下。邮政编码可以属于一个城市、县或州,也可以根据业务需求属于自定义的邮政编码组。 - impeto
5个回答

5
我认为最干净的方法是这样做 - 我对其他发布的答案并不完全满意 - 将是:
 persons.stream().collect(Collectors.toMap(
    person -> person,
    person -> regions.stream()
       .filter(region -> region.getZipCodes().contains(person.getZip()))
       .collect(Collectors.toList())));

这段代码对我来说无法编译。我最初尝试了相同的方法,但在 ...contains(person.getZip()) 处失败,提示找不到 getZip() 方法。它说 person 是一个 lambda 参数。 - MadConan
1
我能获取完整的错误信息吗?你可能需要在lambda的开头写(Person person) ->... - Louis Wasserman
现在看来我似乎无法复制这个问题。不确定问题出在哪里,但是在我关闭IntelliJ并重新启动后,编译问题就消失了。 - MadConan
是的...我也遇到了那个波浪线,但我使用类型提示它就消失了。感谢您的答案。它运行得很好。 - impeto

2

原始答案对元组进行了不必要的映射,因此您可以在那里看到最终解决方案。您可以删除映射,并直接过滤regions列表:

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

如果我理解正确,您可以做类似于这样的事情:
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())));

List<Person> 获取一个 Stream<Person>。然后将每个实例映射到一个包含所有区域的元组 <Person,List<Region>>。从那里,使用 toMap 收集器将数据收集到映射中,并且对于每个人,您构建一个包含该人的邮政编码的 Region 列表。
例如,给定输入:
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")));

它的输出结果为:

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

我猜每个RegionzipCodes可以是一个Set


1
为什么要通过SimpleEntry去呢? - Louis Wasserman
1
@LouisWasserman 是的,这就是我在睡觉时想到的,然后醒来修改它。编辑...啊,我看到你已经回答了... - Alexis C.
从理论上讲,将其设置为Set是有意义的,但实际上数据库中存在约束条件,以确保一个地区没有重复的邮政编码。我总是使用最严格的约束条件设计数据库,这样我就可以在应用程序层面上放松一些 :) - impeto

2

我没有对这段代码进行任何测试,但是它能编译,所以应该是正确的(: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;
}

0

这还是相当丑陋的,我认为通过稍微改变建模方式可以改进它,但到目前为止我只想出了以下内容:

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]}
}

拥有一个包含映射并且可以进行键控的ZipCode类会使事情更加清晰:
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

其他答案中有些包含了很多线性搜索列表的代码。我认为Java 8 Stream解决方案不应该比经典变体慢太多。 因此,这里提供一种利用流的解决方案,而不会牺牲太多性能。

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接