如何基于使用流分组在集合上执行两个不同的功能?

Kri*_*ina 5 java-8 java-stream

我试图使用流重构一些不那么优雅的代码.我有一个包含字符串和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);
Run Code Online (Sandbox Code Playgroud)

因为我只关心id,所以我开始使用map操作来获取id,然后使用filter操作来消除空的.

接下来的部分就是我遇到的麻烦.我尝试的第一件事是使用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));
Run Code Online (Sandbox Code Playgroud)

这个链接有助于减少使用Stream收集器:Stream Reduction

我们在这段代码中至少有两个问题:1)collect是一个终止操作,它将关闭流,我们还没有完成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);
Run Code Online (Sandbox Code Playgroud)

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

Flo*_*own 3

您的Stream方法和常见方法在返回类型方面非常不同。因此,我改变了您以前的 API 方法Stream。

为了减少一些代码,您应该首先构建一个Map<Character, Function<String, MyObject>>在映射步骤中进行简洁查找的代码。
看起来像这样:

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

管道如何运作?

  1. 地图MyObject->MyObject::getConnectedToId
  2. 过滤空Strings
  3. 在 中执行查找serviceMap。如果存在,则返回Function<String, MyObject>,否则id -> 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);
Run Code Online (Sandbox Code Playgroud)

也可以map使用forEach运算直接将计算值相加。

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));
Run Code Online (Sandbox Code Playgroud)

  • @Kristina如果已经存在的值发生冲突,那么默认的“Collectors::toMap”将抛出异常。因此,您必须提供一个合并函数,负责决定采用哪个元素,新值还是旧值(“(mo1, mo2) -&gt; mo2”)。 (2认同)