For loop including if to parallelStream() expression - java

Is there a way to parallelize this piece of code:
HashMap<String, Car> cars;
List<Car> snapshotCars = new ArrayList<>();
...
for (final Car car : cars.values()) {
if (car.isTimeInTimeline(curTime)) {
car.updateCalculatedPosition(curTime);
snapshotCars.add(car);
}
}
Update: This is what I tried before asking for assistance:
snapshotCars.addAll(cars.values().parallelStream()
.filter(c -> c.isTimeInTimeline(curTime))
.collect(Collectors.toList()));
How could I integrate this line? ->
car.updateCalculatedPosition(curTime);

Well, assuming that updateCalculatedPosition does not affect state outside of the Car object on which it runs, it may be safe enough to use peek for this:
List<Car> snapshotCars = cars.values()
.parallelStream()
.filter(c -> c.isTimeInTimeline(curTime))
.peek(c -> c.updateCalculatedPosition(curTime))
.collect(Collectors.toCollection(ArrayList::new));
I say this is "safe enough" because the collect dictates which elements will be peeked by peek, and these will necessarily be all the items that passed the filter. However, read this answer for the reason why peek should generally be avoided for "significant" operations.
Your peek-free alternative is to first, filter and collect, and then update using the finished collection:
List<Car> snapshotCars = cars.values()
.parallelStream()
.filter(c -> c.isTimeInTimeline(curTime))
.collect(Collectors.toCollection(ArrayList::new));
snapShotCars.parallelStream()
.forEach(c -> c.updateCalculatedPosition(curTime));
This is safer from an API point of view, but less parallel - you only start updating the positions after you have finished filtering and collecting.

If you want parallelized access to a List you might want to use Collections.synchonizedList to get a thread-safe list:
List<Car> snapshotCars = Collections.synchronizedList(new ArrayList<>());
Then you can use the stream API like so:
cars.values()
.parallelStream()
.filter(car -> car.isTimeInTimeline(curTime))
.forEach(car -> {
car.updateCalculatedPosition(curTime);
snapshotCars.add(car);
});

In addition to RealSkeptic’s answer, you can alternatively use your own collector:
List<Car> snapshotCars = cars.values().parallelStream()
.filter(c -> c.isTimeInTimeline(curTime))
.collect(ArrayList::new,
(l,c) -> { c.updateCalculatedPosition(curTime); l.add(c); },
List::addAll);
Note that .collect(Collectors.toList()) is equivalent (though not necessarily identical) to .collect(Collectors.toCollection(ArrayList::new)) which is equivalent to .collect(ArrayList::new, List::add, List::addAll).
So our custom collector does a similar operation, but replaces the accumulator with a function, which also performs the desired additional operation.

Related

Java 8 Lambdas flatmapping, groupingBy and mapping to get a Map of T and List<K>

Here's what I have so far:
Map<Care, List<Correlative>> mapOf = quickSearchList
.stream()
.map(QuickSearch::getFacility)
.collect(Collectors.flatMapping(facility -> facility.getFacilityCares().stream(),
Collectors.groupingBy(FacilityCare::getCare,
Collectors.mapping(c -> {
final Facility facility = new Facility();
facility.setId(c.getFacilityId());
return Correlative.createFromFacility(facility);
}, Collectors.toList()))));
I have a list of Quick Searches to begin with. Each item in the quick search has a single facility as in:
public class QuickSearch {
Facility facility;
}
In every Facility, there's a List of FacilityCare as in:
public class Facility {
List<FacilityCare> facilityCares;
}
And finally, FacilityCare has Care property as in:
public class FacilityCare {
Care care;
}
Now, the idea is to convert a List of QuickSearch to a Map of <Care, List<Correlative>>.
The code within the mapping() function is bogus, in the example above. FacilityCare only has facilityID and not Facility entity. I want the facility object that went as param in flatMapping to be my param again in mapping() function as in:
Collectors.mapping(c -> Correlative.createFromFacility(facility))
where "facility" is the same object as the one in flatMapping.
Is there any way to achieve this? Please let me know if things need to be explained further.
Edit:
Here's a solution doesn't fully utilize Collectors.
final Map<Care, List<Correlative>> mapToHydrate = new HashMap<>();
quickSearchList
.stream()
.map(QuickSearch::getFacility)
.forEach(facility -> {
facility.getFacilityCares()
.stream()
.map(FacilityCare::getCare)
.distinct()
.forEach(care -> {
mapToHydrate.computeIfAbsent(care, care -> new ArrayList<>());
mapToHydrate.computeIfPresent(care, (c, list) -> {
list.add(Correlative.createFromFacility(facility));
return list;
});
});
});
Sometimes, streams are not the best solution. This seems to be the case, because you are losing each facility instance when going down the pipeline.
Instead, you could do it as follows:
Map<Care, List<Correlative>> mapToHydrate = new LinkedHashMap<>();
quickSearchList.forEach(q -> {
Facility facility = q.getFacility();
facility.getFacilityCares().forEach(fCare ->
mapToHydrate.computeIfAbsent(fCare.getCare(), k -> new ArrayList<>())
.add(Correlative.createFromFacility(facility)));
});
This uses the return value of Map.computeIfAbsent (which is either the newly created list of correlatives or the already present one).
It is not clear from your question why you need distinct cares before adding them to the map.
EDIT: Starting from Java 16, you might want to use Stream.mapMulti:
Map<Care, List<Correlative>> mapToHydrate = quickSearchList.stream()
.map(QuickSearch::getFacility)
.mapMulti((facility, consumer) -> facility.getFacilityCares()
.forEach(fCare -> consumer.accept(Map.entry(fCare.getCare(), facility))))
.collect(Collectors.groupingBy(
e -> e.getKey(),
Collectors.mapping(
e -> Correlative.createFromFacility(e.getValue()),
Collectors.toList())));
This is what I came up with based on the information provided. The Facility and Care are stored in a temp array to be processed later in the desired map.
Map<Care, List<Correlative>> mapOf = quickSearchList.stream()
.map(QuickSearch::getFacility)
.flatMap(facility -> facility
.getFacilityCares().stream()
.map(facCare->new Object[]{facility, facCare.getCare()}))
.collect(Collectors.groupingBy(obj->(Care)obj[1], Collectors
.mapping(obj -> Correlative.createFromFacility(
(Facility)obj[0]),
Collectors.toList())));
I prepared some simple test data and this seems to work assuming I understand the ultimate goal. For each type of care offered, it puts all the facilities that offer that care in an associated list of facilities.
Inspired by #fps answer, I was able to come up with a solution that will work for the time being (pre-Java16).
Map<Care, List<Correlative>> mapOf = quickSearchList
.stream()
.map(QuickSearch::getFacility)
.map(expandIterable())
.collect(
Collectors.flatMapping(map -> map.entrySet().stream(),
Collectors.groupingBy(Map.Entry::getKey,
Collectors.mapping(entry -> Correlative.createFromFacility(entry.getValue()),
Collectors.toList()
)
)
));
}
public Function<Facility, Map<Care, Facility>> expandIterable() {
return facility -> facility.getFacilityCares()
.stream()
.map(FacilityCare::getCare)
.distinct()
.collect(Collectors.toMap(c -> c, c -> facility));
}
Basically, I added a method call that returns a Function that takes in Facility as argument and returns a Map of Care as key with Facility as value. That map is used in the collection of the previous stream.

After flattening a Java 8 multilevel map my expected result is empty

I have the below multilevel map:
Map<String, List<Map<String, Map<String, Map<String, Map<String, String>>>>>> input =
ImmutableMap.of("A",
ImmutableList.of(ImmutableMap.of("2",
ImmutableMap.of("3",
ImmutableMap.of("4",
ImmutableMap.of("5", "a"))))));
In short it'll be like
{
"A":[{"2":{"3":{"4":{"5":"a"}}}}],
"B":[{"2":{"3":{"4":{"5":"b"}}}}]
}
My requirement is to construct a map of the form
{
"A":"a",
"B":"b"
}
I tried the below code but for some reason myMap is always empty even though I'm populating it. What am I missing?
Map<String, String> myMap = new HashMap<>();
input.entrySet()
.stream()
.map(l -> l.getValue().stream().map(m -> m.get(m.keySet().toArray()[0]))
.map(n -> n.get(n.keySet().toArray()[0]))
.map(o -> o.get(o.keySet().toArray()[0]))
.map(p -> myMap.put(l.getKey(), p.get(p.keySet().toArray()[0])))).collect(Collectors.toList());
System.out.println(myMap);
Here's what I get when I add two peek calls to your pipeline:
input.entrySet().stream()
.peek(System.out::println) //<- this
.map(l -> ...)
.peek(System.out::println) //<- and this
.collect(Collectors.toList());
output:
A=[{2={3={4={5=a}}}}]
java.util.stream.ReferencePipeline$3#d041cf
If you notice the problem, you're collecting streams, and these streams don't get executed through a call to a terminal operation... When I try adding something like .count() to the inner stream, your expected output is produced:
...
.map(l -> l.getValue().stream().map(m -> m.get(m.keySet().toArray()[0]))
.map(n -> n.get(n.keySet().toArray()[0]))
.map(o -> o.get(o.keySet().toArray()[0]))
.map(p -> myMap.put(l.getKey(), p.get(p.keySet().toArray()[0])))
.count()) //just an example
...
Now, I suppose you know that a terminal operation needs to be called for the intermediate ones to run.
In a rather desperate attempt to simplify this code, as the stream seems to make it simply hard to read, I thought you might be interested in this, which assumes that no collection is empty in the tree but at least addrsses the record as one object, and not a collection of records (but I'm sure no code will look clean for that deep map of of maps).
String key = input.keySet().iterator().next();
String value = input.entrySet().iterator().next()
.getValue().get(0)
.values().iterator().next()
.values().iterator().next()
.values().iterator().next()
.values().iterator().next();
myMap.put(key, value);

Elegant way to flatMap Set of Sets inside groupingBy

So I have a piece of code where I'm iterating over a list of data. Each one is a ReportData that contains a case with a Long caseId and one Ruling. Each Ruling has one or more Payment. I want to have a Map with the caseId as keys and sets of payments as values (i.e. a Map<Long, Set<Payments>>).
Cases are not unique across rows, but cases are.
In other words, I can have several rows with the same case, but they will have unique rulings.
The following code gets me a Map<Long, Set<Set<Payments>>> which is almost what I want, but I've been struggling to find the correct way to flatMap the final set in the given context. I've been doing workarounds to make the logic work correctly using this map as is, but I'd very much like to fix the algorithm to correctly combine the set of payments into one single set instead of creating a set of sets.
I've searched around and couldn't find a problem with the same kind of iteration, although flatMapping with Java streams seems like a somewhat popular topic.
rowData.stream()
.collect(Collectors.groupingBy(
r -> r.case.getCaseId(),
Collectors.mapping(
r -> r.getRuling(),
Collectors.mapping(ruling->
ruling.getPayments(),
Collectors.toSet()
)
)));
Another JDK8 solution:
Map<Long, Set<Payment>> resultSet =
rowData.stream()
.collect(Collectors.toMap(p -> p.Case.getCaseId(),
p -> new HashSet<>(p.getRuling().getPayments()),
(l, r) -> { l.addAll(r);return l;}));
or as of JDK9 you can use the flatMapping collector:
rowData.stream()
.collect(Collectors.groupingBy(r -> r.Case.getCaseId(),
Collectors.flatMapping(e -> e.getRuling().getPayments().stream(),
Collectors.toSet())));
The cleanest solution is to define your own collector:
Map<Long, Set<Payment>> result = rowData.stream()
.collect(Collectors.groupingBy(
ReportData::getCaseId,
Collector.of(HashSet::new,
(s, r) -> s.addAll(r.getRuling().getPayments()),
(s1, s2) -> { s1.addAll(s2); return s1; })
));
Two other solutions to which I thought first but are actually less efficient and readable, but still avoid constructing the intermediate Map:
Merging the inner sets using Collectors.reducing():
Map<Long, Set<Payment>> result = rowData.stream()
.collect(Collectors.groupingBy(
ReportData::getCaseId,
Collectors.reducing(Collections.emptySet(),
r -> r.getRuling().getPayments(),
(s1, s2) -> {
Set<Payment> r = new HashSet<>(s1);
r.addAll(s2);
return r;
})
));
where the reducing operation will merge the Set<Payment> of entries with the same caseId. This can however cause a lot of copies of the sets if you have a lot of merges needed.
Another solution is with a downstream collector that flatmaps the nested collections:
Map<Long, Set<Payment>> result = rowData.stream()
.collect(Collectors.groupingBy(
ReportData::getCaseId,
Collectors.collectingAndThen(
Collectors.mapping(r -> r.getRuling().getPayments(), Collectors.toList()),
s -> s.stream().flatMap(Set::stream).collect(Collectors.toSet())))
);
Basically it puts all sets of matching caseId together in a List, then flatmaps that list into a single Set.
There are probably better ways to do this, but this is the best I found:
Map<Long, Set<Payment>> result =
rowData.stream()
// First group by caseIds.
.collect(Collectors.groupingBy(r -> r.case.getCaseId()))
.entrySet().stream()
// By streaming over the entrySet, I map the values to the set of payments.
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().stream()
.flatMap(r -> r.getRuling().getPayments().stream())
.collect(Collectors.toSet())));

Collect more lists from a single stream and then merge them all

Is there a more elegant way collect more lists from a single stream and then merge them all?
This is what I came up with:
private Set<Long> getAllIds(List<MyClass> fetchedElements) {
Set<Long> myIds = new HashSet<>();
myIds.addAll(fetchedItems.stream().map(MyClass::getBlueId).collect(Collectors.toList()));
myIds.addAll(fetchedItems.stream().map(MyClass::getRedId).collect(Collectors.toList()));
myIds.addAll(fetchedItems.stream().map(MyClass::getGreenId).collect(Collectors.toList()));
return myIds;
}
It seems to me when I look at these three lines of code as there is code duplication there, but I didn't find a way to join the different map operations I'm doing with the stream.
Thanks for helping!
Use a flatMap!!
fetchedElements.stream()
.flatMap(item -> Stream.of(item.getBlueId(), item.getRedId(), item.getGreenId()))
.collect(Collectors.toSet());
Note that the order of evaluation BRGBRG rather than BBRRGG, the output order is of course undefined as you are using a Set. I assume this doesn't matter.
You could make this method more general:
private Set<Long> getAllIds(List<MyClass> fetchedElements,
List<Function<MyClass, Long>> extractors) {
return fetchedElements.stream()
.flatMap(item -> extractors.stream().map(f -> f.apply(item)))
.collect(Collectors.toSet());
}
Call with:
Set<Long> myIds = getAllIds(fetchedElements, List.of(MyClass::getBlueId, MyClass::getRedId, MyClass::getGreenId))
If you want the other evaluation order, swap the order of operations:
extractors.stream()
.flatMap(f -> fetchedElements.stream().map(item -> f.apply(item))
.collect(Collectors.toSet()));

How to remove an element of a HashMap whilst streaming (lambda)

I have the following situation where I need to remove an element from a stream.
map.entrySet().stream().filter(t -> t.getValue().equals("0")).
forEach(t -> map.remove(t.getKey()));
in pre Java 8 code one would remove from the iterator - what's the best way to deal with this situation here?
map.entrySet().removeIf(entry -> entry.getValue().equals("0"));
You can't do it with streams, but you can do it with the other new methods.
EDIT: even better:
map.values().removeAll(Collections.singleton("0"));
If you want to remove the entire key, then use:
myMap.entrySet().removeIf(map -> map.getValue().containsValue("0"));
I think it's not possible (or deffinitelly shouldn't be done) due to Streams' desire to have Non-iterference, as described here
If you think about streams as your functional programming constructs leaked into Java, then think about the objects that support them as their Functional counterparts and in functional programming you operate on immutable objects
And for the best way to deal with this is to use filter just like you did
1st time replying. Ran across this thread and thought to update if others are searching. Using streams you can return a filtered map<> or whatever you like really.
#Test
public void test() {
Map<String,String> map1 = new HashMap<>();
map1.put("dan", "good");
map1.put("Jess", "Good");
map1.put("Jaxon", "Bad");
map1.put("Maggie", "Great");
map1.put("Allie", "Bad");
System.out.println("\nFilter on key ...");
Map<String,String> map2 = map1.entrySet().stream().filter(x ->
x.getKey().startsWith("J"))
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
map2.entrySet()
.forEach(s -> System.out.println(s));
System.out.println("\nFilter on value ...");
map1.entrySet().stream()
.filter(x -> !x.getValue().equalsIgnoreCase("bad"))
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()))
.entrySet().stream()
.forEach(s -> System.out.println(s));
}
------- output -------
Filter on key ...
Jaxon=Bad
Jess=Good
Filter on value ...
dan=good
Jess=Good
Maggie=Great

Categories

Resources