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.
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?
MyObject
-> MyObject::getConnectedToId
Strings
serviceMap
. If it is present, then return the Function<String, MyObject>
, else id -> null
null
valuesMap<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));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With