Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I perform two different functions on a collection based on grouping with a stream?

I am trying to refactor some not so elegant code using a stream. I have a HashMap containing Strings and MyObjects and currently iterating over it using a for loop like so:

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

Since I only care about the ids, I started by using the map operation to get only the ids, followed by a filter operation to eliminate an empty ones.

The next part is what I am having trouble with. The first thing I tried was using the Collectors groupingBy operation so that I could group the items based on the first character of the id and I ended up with this:

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

This link helped with the reduction using Stream collectors: Stream Reduction

We have at least two problems with this code: 1) collect is a terminal operation which will close the stream and we haven't finished yet and 2) We still need the original object but it has now been reduced to a map of connectedToIds.

Q1) Is there an intermediate operation that will allow us to group the objects based on the first character of the id?

Q2) How can we do this without reducing the collection to only the ids?

Q3) And lastly, once the collection has been grouped (there will be two), how can we perform separate functions on each group as in the original code?


Final Solution (thanks to @Holger & @Flown for the help)

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

To avoid a concurrent modification exception, it is necessary to first store the objects in a temporary map while performing the stream operations and then once finished, add them to the final map.

like image 657
Kristina Avatar asked Oct 19 '22 05:10

Kristina


1 Answers

Your Stream approach and your common approach are very different in terms of return types. Therefore I transformed your former approach to the Stream API.

To reduce some of your code you should first build a Map<Character, Function<String, MyObject>> to do a concise lookup in a mapping step.
Looks something like this:

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

How does the pipeline work?

  1. map MyObject -> MyObject::getConnectedToId
  2. filter empty Strings
  3. perform a lookup in the serviceMap. If it is present, then return the Function<String, MyObject>, else id -> null
  4. filter null values
  5. last step is to collect the result by providing the right extractor functions

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

It is also possible to add the computed values directly to map by using forEach operation.

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));
like image 165
Flown Avatar answered Oct 21 '22 02:10

Flown