【问题标题】:How can I perform two different functions on a collection based on grouping with a stream?如何基于流分组对集合执行两个不同的功能?
【发布时间】:2016-07-12 17:24:23
【问题描述】:

我正在尝试使用流重构一些不太优雅的代码。我有一个包含字符串和 MyObjects 的 HashMap,目前使用 for 循环对其进行迭代,如下所示:

Map<String, MyObject> map = new HashMap<>();
Map<String, MyObject> objectsToAdd = new HashMap<>();


for(MyObject object : map.values()){
        String idToAdd = object.getConnectedToId();

        if(StringUtils.isEmpty(idToAdd) {
            continue;
        }

        if(idToAdd.substring(0,1).equals("i")){ // connected to an ICS
            MyObject newObject = service1.someMethod(idToAdd);

            if(newObject != null) {
                objectsToAdd.put(newObject.getId(), newObject);
            }
        } else if (idToAdd.substring(0,1).equals("d")){ // connected to a device
            MyObject newObject = service2.someMethod(idToAdd);
            if(newObject != null) {
                objectsToAdd.put(newObject.getId(), newObject);
            }
        }

    }

    map.putAll(objectsToAdd);

由于我只关心 id,所以我首先使用 map 操作只获取 id,然后使用过滤器操作来消除空的。

下一部分是我遇到的麻烦。我尝试的第一件事是使用 Collectors groupingBy 操作,以便我可以根据 id 的第一个字符对项目进行分组,我最终得到了:

        map.values().stream()
            .map(myObject -> myObject.getConnectedToId()) // get a map of all the ids
            .filter(StringUtils::isNotEmpty) // filter non empty ones
            .collect(
                Collectors.mapping(
                    MyObject::getId,
                    Collectors.toList())),
                        Collectors.groupingBy(
                            s -> s.substring(0,1));

此链接有助于减少使用流收集器:Stream Reduction

这段代码至少有两个问题:1)collect 是一个terminal operation,它将关闭流,我们还没有完成;2)我们仍然需要原始对象,但现在它已被简化为connectedToIds 的地图。

Q1) 是否有中间操作允许我们根据 id 的第一个字符对对象进行分组?

Q2)我们如何在不将集合减少到仅 ID 的情况下做到这一点?

Q3) 最后,一旦集合被分组(会有两个),我们如何像原始代码一样在每个组上执行单独的功能?


最终解决方案(感谢@Holger 和@Flown 的帮助)

    Map<Character, Function<String, MyObejct>> methodMapping = new HashMap<>();
    methodMapping.put('i', service1::method1);
    methodMapping.put('d', service2::method2);

    Map<String, MyObject> toAdd = map.values().stream().map(MyObject::getConnectedToId)
        .filter(StringUtils::isNotEmpty)
        .map(id -> methodMapping.getOrDefault(id.charAt(0), i -> null).apply(id))
        .filter(Objects::nonNull)
        .collect(Collectors.toMap(MyObject::getId, Function.identity(), (mo1, mo2) -> mo2));

    map.putAll(toAdd);

为避免并发修改异常,有必要在执行流操作时首先将对象存储在临时映射中,然后在完成后将它们添加到最终映射中。

【问题讨论】:

  • 我假设map的key是MyObjectid? IE。 map.get(object.getId()).equals(object) == true
  • 是的,HashMap中的key就是MyObject的id。为了清楚起见,我对问题进行了小幅编辑 - idToAdd 是 myObject 上的 connectedToId,而不是对象的 id。

标签: java-8 java-stream


【解决方案1】:

您的Stream 方法和您的常用方法在返回类型方面非常不同。因此,我将您以前的方法转换为 Stream API。

要减少一些代码,您应该首先构建一个Map&lt;Character, Function&lt;String, MyObject&gt;&gt;,以便在映射步骤中进行简洁的查找。
看起来像这样:

Map<Character, Function<String, MyObject>> serviceMapping = new HashMap<>();
serviceMapping.put('i', service1);
serviceMapping.put('d', service2);

管道是如何工作的?

  1. 映射MyObject -> MyObject::getConnectedToId
  2. 过滤空Strings
  3. serviceMap 中执行查找。如果存在,则返回Function&lt;String, MyObject&gt;,否则返回id -&gt; null
  4. 过滤null
  5. 最后一步是通过提供正确的提取器函数来收集结果

Map<String, MyObject> toAdd = map.values().stream().map(MyObject::getConnectedToId)
    .filter(StringUtils::isEmpty)
    .map(id -> serviceMapping.getOrDefault(id.charAt(0), i -> null).apply(id))
    .filter(Objects::nonNull)
    .collect(Collectors.toMap(MyObject::getId, Function.identity(), (mo1, mo2) -> mo2));
map.putAll(toAdd);

也可以使用forEach 操作将计算值直接添加到map

map.values().stream().map(MyObject::getConnectedToId)
    .filter(StringUtils::isEmpty)
    .map(id -> serviceMapping.getOrDefault(id.charAt(0), i -> null).apply(id))
    .filter(Objects::nonNull)
    .forEach(mo -> map.put(mo.getId(), mo));

【讨论】:

  • 是否假定mo1 和mo2 是method1 和method2?此外,在最后一次收集操作中,我得到“无法从静态上下文引用非静态方法”
  • @Kristina 如果已经存在的值发生冲突,那么默认的Collectors::toMap 将引发异常。因此,您必须提供一个合并函数,该函数负责决定采用哪个元素,新值还是旧值 ((mo1, mo2) -&gt; mo2)。
  • @Holger 直到map 操作正常为止。我不得不对serviceMapping 进行一项修改,因为它需要返回MyObject 的函数。 Map&lt;Character, Function&lt;String, DeviceMapObject&gt;&gt; methodMapping = new HashMap&lt;&gt;(); methodMapping.put('i', service1::getMOById); methodMapping.put('d', service2::getMOById); 这看起来对吗?在这种情况下似乎没有调用这些方法,而是 map 操作没有返回任何结果
  • @Kristina:你没有告诉我们任何关于service1service2 的事情。如果它们实现相同的接口或具有定义getMOById 方法的通用基类,则可以简化解决方案。
  • @Holger:不幸的是,它们没有实现相同的接口或具有共同的基类。每个服务返回两个不同的 DTO 对象,然后在返回之前用 MyObject 包装。
猜你喜欢
  • 2011-01-16
  • 1970-01-01
  • 1970-01-01
  • 2021-03-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-01
  • 1970-01-01
相关资源
最近更新 更多