在Java 8中,有没有一种方法将分组列表连接成一个集合?

7
我处于一种奇怪的情况下,有一个JSON API,需要一个字符串数组来作为键,和一个餐厅字符串数组作为值。这些值会被GSON解析成一个“餐厅对象”,该对象定义了一个代表社区的字符串和一个包含餐厅名称的列表。系统将这些数据存储在一个映射中,其中键是社区名称,值是该社区内餐厅名称的集合。因此,我想实现一个函数,它可以从API输入中获取数据并按社区分组,并将餐厅列表连接起来。
由于受Java 8的限制,我无法使用更高级的功能(如flatMapping)在一行代码中完成所有操作。我找到的最佳解决方案是使用一个中间映射来存储List的集合,然后将这些列表连接成一个集合,并将它们作为值存储在最终映射中。
public Map<String, Set<String>> parseApiEntriesIntoMap(List<Restaurant> restaurants) {
    if(restaurants == null) {
      return null;
    }
    Map<String, Set<String>> restaurantListByNeighborhood = new HashMap<>();
    // Here we group by neighborhood and concatenate the list of restaurants into a set
    Map<String, Set<List<String>>> map =
        restaurants.stream().collect(groupingBy(Restaurant::getNeighborhood,
                              Collectors.mapping(Restaurant::getRestaurantList, toSet())));
    map.forEach((n,r) -> restaurantListByNeighborhood.put(n, Sets.newHashSet(Iterables.concat(r))));

    return restaurantListByNeighborhood;
  }

我觉得一定有办法摆脱中间的map,把所有操作都放在一行里完成…有没有更好的解决方案可以让我这样做?


2
请注意,“一行代码”和“使用流”都不意味着“更好”。传统的循环也没有问题。此外,如果getRestaurantList()返回餐厅连锁店的各个餐厅的名称,那么这似乎有些过度,因为在给定的社区中找到一个连锁店的实例超出了极限,因此更好的模型是在Restaurant上拥有一个chain属性,如果您真的关心的话,并且只需让每个Restaurant实例拥有自己的条目即可。如果不是这样,那么它是什么意思?为了清晰起见,Restaurant应该改名为RestaurantChain吗? - Bohemian
2个回答

3
您可以使用Java-8中的toMap,并定义一个mergeFunction,实现该功能。
public Map<String, Set<String>> parseApiEntriesIntoMap(List<Restaurant> restaurants) {
    // read below about the null check
    return restaurants.stream()
            .collect(Collectors.toMap(Restaurant::getNeighborhood,
                    r -> new HashSet<>(r.getRestaurantList()), (set1, set2) -> {
                        set1.addAll(set2);
                        return set1;
                    }));
}

除此之外,您应确保从方法的第一个代码块中检查和获取结果。
if(restaurants == null) {
  return null;
}

另一方面,处理空的CollectionMap时,由于流和collect操作本身的性质,上述代码将返回一个空Map,因此它应该是多余的。

注意:此外,如果您未来的升级需要更相关的flatMapping代码,可以使用此答案中提供的实现。


或者在不使用流的情况下解决问题,此时的解决方案看起来类似于使用Map.merge的方法。 它将使用类似的BiFunction

public Map<String, Set<String>> parseApiEntriesIntoMap(List<Restaurant> restaurants) {
    Map<String, Set<String>> restaurantListByNeighborhood = new HashMap<>();
    for (Restaurant restaurant : restaurants) {
        restaurantListByNeighborhood.merge(restaurant.getNeighborhood(),
                new HashSet<>(restaurant.getRestaurantList()),
                (strings, strings2) -> {
                    strings.addAll(strings2);
                    return strings;
                });
    }
    return restaurantListByNeighborhood;
}

1
谢谢你的回答!我没有意识到我也可以使用 Map.merge 来按邻居分组,而且空值检查是不必要的。 - creposukre
当传递给方法的List被确保为空或填充有值时,空检查将不是必需的。这应该是使用集合的一般惯例(可以避免不必要的空检查和NPE)。 - Naman
2
我宁愿在循环中使用 restaurantListByNeighborhood.computeIfAbsent(restaurant.getNeighborhood(), key -> new HashSet<>()).addAll(restaurant.getRestaurantList());… 既然你提到了 flatMapping,也可以提及完整的解决方案,即 restaurants.stream().collect(groupingBy(Restaurant::getNeighborhood, flatMapping(r -> r.getRestaurantList().stream(), toSet())));,因为我在这个问答中看到过。 - Holger

2
你可以使用Collectors.collectingAndThen来收集Set<List<String>>并将其压平。"最初的回答"。
Map<String, Set<String>> res1 = list.stream()
            .collect(Collectors.groupingBy(Restaurant::getNeighborhood,
            Collectors.mapping(Restaurant::getRestaurantList, 
                    Collectors.collectingAndThen(Collectors.toSet(), 
                            set->set.stream().flatMap(List::stream).collect(Collectors.toSet())))));

为什么要在这里引入“joining”?这将会把一个集合中的所有元素连接成一个字符串,然后将其他类似的出现添加到最终的Set中。 - Naman
Sets.newHashSet(Iterables.concat(r))Set<List<String>> 扁平化为 Set<String>,但不是通过连接 List 中的字符串来实现。例如,Set.of(List.of("one", "two"), List.of("three","four")) 将变成 Set.of("one", "two","three","four"),而不是像 Set.of("one two", "three four") 这样的结果。 - Naman
是的,我的意思是将 Set<List<String>> 扁平化为 Set<String>,集合中列表的元素被提取出来成为 Set 的元素。 - creposukre
1
太好了!这就是我一直在寻找的解决方案,谢谢!我曾考虑使用 Collectors.collectingAndThen,但无法理解如何使用它来实现所需的结果。 - creposukre

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