Layered filtering using Java Stream API - java

I have some imperative Java conditional code that I want to refactor to use Streams.
Specifically, I have this map that I want to filter into a List based on specific filter criteria.
private Map<Integer,Thing> thingMap = new HashMap<Integer,Thing>();
// populate thingMap
And here's the code that uses it:
List<Thing> things = new ArrayList<Thing>();
for (Thing thing : thingMap.values()) {
if (thing.getCategory().equals(category)) {
if (location == null) {
things.add(thing);
} else if (thing.getLocation().equals(location)) {
things.add(thing);
}
}
}
I refactored that to the following. But what's missing is I want the location to be checked only if the category filter passes. Also, I suspect there's a better way to do this:
List<Thing> things = thingMap.entrySet()
.stream()
.filter(t -> t.getValue().getCategory().equals(category))
.filter(t ->
location == null ||
t.getValue().getLocation().equals(location)
)
.map(Map.Entry::getValue)
.collect(Collectors.toList());
What would be the idiomatic approach to retaining the layered conditional checks using Streams?

Operations chained after a filter will only be executed for elements accepted by the predicate. So there is no need to worry about that.
You could also join the conditions into a single filter step, just like you could join the nested if statements into a single if, by combining the conditions using &&. The result is the same.
But note that the loop uses the condition location == null, referring to the variable declared outside the code snippet you have posted, not thing.getLocation() == null.
Besides that, you made other unnecessary changes compared to the loop. The loop iterates over the values() view of the map whereas you used entrySet() for the Stream instead, introducing the need to call getValue() on a Map.Entry four times.
A straight-forward translation of the loop logic is much simpler:
List<Thing> things = thingMap.values().stream()
.filter(thing -> thing.getCategory().equals(category))
.filter(thing -> location == null || thing.getLocation().equals(location))
.collect(Collectors.toList());

Related

Java Stream - Collecting Parent-objects into a Set after applying flatMap()

Is it possible to go back to the parent-object after applying flatmap() operation, and accumulate parent-objects into a set?
I have an UnifiedOfferEntity with set of other entity objects as a field:
public static class UnifiedOfferEntity {
private Set<ContractDetailsEntity> contractDetails;
// getters, etc.
}
I would like to filter through fields of the parent-object (UnifiedOfferEntity) like this:
offersFromDB.stream()
.filter(offer -> CollectionUtils.containsAny(preferences.getOwnedSkills(), offer.getSkills())
&& CollectionUtils.containsAny(preferences.getOwnedSeniority(), offer.getSeniority()))
And then I would like to examine the nested collection, filter through child-objects (ContractDetailsEntity):
.flatMap(offer -> offer.getContractDetails().stream()
.filter(cd -> cd.getSalaryFrom() >= preferences.getSalaryMin())
And finally, I need to move back to the parent-object and collect its instances into a Set after these filters.
I was trying with this:
List<UnifiedOfferEntity> offersFromDB = // initializing somehow
Set<UnifiedOfferEntity> result = offersFromDB.stream()
.filter(offer -> CollectionUtils.containsAny(preferences.getOwnedSkills(), offer.getSkills())
&& CollectionUtils.containsAny(preferences.getOwnedSeniority(), offer.getSeniority()))
.flatMap(offer -> offer.getContractDetails().stream()
.filter(cd -> cd.getSalaryFrom() >= preferences.getSalaryMin() &&
cd.getSalaryTo() <= preferences.getSalaryMax() &&
tocPreferences.contains(cd.getTypeOfContract())))
.collect(Collectors.toSet())
But it creates a Set of ContractDetailsEntity, not UnifiedOfferEntity. How can I fix this?
You can perform filtering based on the contents of the nested collection (a set of ContractDetailsEntity) and then accumulate enclosing objects (ContractDetails) for which provided Predicate has been evaluated to true by using built-in collector filtering(), which expects a predicate and a downstream collector.
If I understood correctly, you need only instances of UnifiedOfferEntity which have all the ContractDetailsEntity that match a particular Predicate. To filter such offers, you can generate a stream of contractDetails and apply allMatch() with all the conditions you've listed (in case if it's sufficient when only one instance of ContractDetailsEntity meets the conditions - apply anyMatch() instead).
That's how it might look like:
List<UnifiedOfferEntity> offersFromDB = // initializing offersFromDB
Set<UnifiedOfferEntity> result = offersFromDB.stream()
.filter(offer -> CollectionUtils.containsAny(preferences.getOwnedSkills(), offer.getSkills())
&& CollectionUtils.containsAny(preferences.getOwnedSeniority(), offer.getSeniority()))
.collect(Collectors.filtering(
offer -> offer.getContractDetails().stream().allMatch(cd -> // a Predicate for evalueating ContractDetails goes here
cd.getSalaryFrom() >= preferences.getSalaryMin()
&& cd.getSalaryTo() <= preferences.getSalaryMax()
&& tocPreferences.contains(cd.getTypeOfContract())),
Collectors.toSet()
);

Aggregate values and convert into single type within the same Java stream

I have a class with a collection of Seed elements. One of the method's return type of Seed is Optional<Pair<Boolean, String>>.
I'm trying to loop over all seeds, find if any boolean value is true and at the same time, create a set with all the String values. For instance, my input is in the form Optional<Pair<Boolean, String>>, the output should be Optional<Signal> where Signal is like:
class Signal {
public boolean exposure;
public Set<String> alarms;
// constructor and getters (can add anything to this class, it's just a bag)
}
This is what I currently have that works:
// Seed::hadExposure yields Optional<Pair<Boolean, String>> where Pair have key/value or left/right
public Optional<Signal> withExposure() {
if (seeds.stream().map(Seed::hadExposure).flatMap(Optional::stream).findAny().isEmpty()) {
return Optional.empty();
}
final var exposure = seeds.stream()
.map(Seed::hadExposure)
.flatMap(Optional::stream)
.anyMatch(Pair::getLeft);
final var alarms = seeds.stream()
.map(Seed::hadExposure)
.flatMap(Optional::stream)
.map(Pair::getRight)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
return Optional.of(new Signal(exposure, alarms));
}
Now I have time to make it better because Seed::hadExposure could become and expensive call, so I was trying to see if I could make all of this with only one pass. I've tried (some suggestions from previous questions) with reduce, using collectors (Collectors.collectingAndThen, Collectors.partitioningBy, etc.), but nothing so far.
It's possible to do this in a single stream() expression using map to convert the non-empty exposure to a Signal and then a reduce to combine the signals:
Signal signal = exposures.stream()
.map(exposure ->
new Signal(
exposure.getLeft(),
exposure.getRight() == null
? Collections.emptySet()
: Collections.singleton(exposure.getRight())))
.reduce(
new Signal(false, new HashSet<>()),
(leftSig, rightSig) -> {
HashSet<String> alarms = new HashSet<>();
alarms.addAll(leftSig.alarms);
alarms.addAll(rightSig.alarms);
return new Signal(
leftSig.exposure || rightSig.exposure, alarms);
});
However, if you have a lot of alarms it would be expensive because it creates a new Set and adds the new alarms to the accumulated alarms for each exposure in the input.
In a language that was designed from the ground-up to support functional programming, like Scala or Haskell, you'd have a Set data type that would let you efficiently create a new set that's identical to an existing set but with an added element, so there'd be no efficiency worries:
filteredSeeds.foldLeft((false, Set[String]())) { (result, exposure) =>
(result._1 || exposure.getLeft, result._2 + exposure.getRight)
}
But Java doesn't come with anything like that out of the box.
You could create just a single Set for the result and mutate it in your stream's reduce expression, but some would regard that as poor style because you'd be mixing a functional paradigm (map/reduce over a stream) with a procedural one (mutating a set).
Personally, in Java, I'd just ditch the functional approach and use a for loop in this case. It'll be less code, more efficient, and IMO clearer.
If you have enough space to store an intermediate result, you could do something like:
List<Pair<Boolean, String>> exposures =
seeds.stream()
.map(Seed::hadExposure)
.flatMap(Optional::stream)
.collect(Collectors.toList());
Then you'd only be calling the expensive Seed::hadExposure method once per item in the input list.

Java Stream > Is is possible to inline an "orElseGet" into a parent Stream?

I wasn't sure how exactly to frame this question, so bear with me...
1) Is there a better (aka more "proper") way to instantiate a Stream of optional elements, other than adding null and subsequently filtering out null's?
Stream.of( ... ,
person.likesRed() ? Color.RED : null)
.filter(Objects::nonNull)
...
2) Secondly, is there a way to "inline" the following orElseGet function into the parent Stream/map?
.map(p -> ofNullable(p.getFavouriteColours()).orElseGet(fallbackToDefaultFavouriteColours))
The full (contrived) example:
import static java.util.Optional.ofNullable;
public Response getFavouriteColours(final String personId) {
Person person = personService.findById(personId);
Supplier<List<String>> fallbackToDefaultFavouriteColours = () ->
Stream.of(
Color.BLUE,
Color.GREEN,
person.likesRed() ? Color.RED : null)
.filter(Objects::nonNull)
.map(Color::getName)
.collect(Collectors.toList());
return ofNullable(person)
.map(p -> ofNullable(p.getFavouriteColours()).orElseGet(fallbackToDefaultFavouriteColours))
.map(Response::createSuccess)
.orElse(Response::createNotFound);
}
A cleaner expression would be
Stream.concat(Stream.of(Color.BLUE, Color.GREEN),
person.likesRed()? Stream.of(Color.RED): Stream.empty())
This isn’t simpler than your original expression, but it doesn’t create the bad feeling of inserting something just to filter it out afterwards or, more abstract, of discarding an already known information that has to be reconstructed afterwards.
There is even a technical difference. The expression above creates a Stream that a has a known size that can be used to optimize certain operations. In contrast, the variant using filter only has an estimated size, which will be the number of elements before filtering, but not a known exact size.
The surrounding code can be greatly simplified by not overusing Optional:
public Response getFavouriteColours(final String personId) {
Person person = personService.findById(personId);
if(person == null) return Response.createNotFound();
List<String> favouriteColours = person.getFavouriteColours();
if(favouriteColours == null)
favouriteColours = Stream.concat(
Stream.of(Color.BLUE, Color.GREEN),
person.likesRed()? Stream.of(Color.RED): Stream.empty())
.map(Color::getName)
.collect(Collectors.toList());
return Response.createSuccess(favouriteColours);
}
Even the Stream operation itself is not simpler than a conventional imperative code here:
public Response getFavouriteColours(final String personId) {
Person person = personService.findById(personId);
if(person==null) return Response.createNotFound();
List<String> favouriteColours = person.getFavouriteColours();
if(favouriteColours==null) {
favouriteColours=new ArrayList<>();
Collections.addAll(favouriteColours, Color.BLUE.getName(), Color.GREEN.getName());
if(person.likesRed()) favouriteColours.add(Color.RED.getName());
}
return Response.createSuccess(favouriteColours);
}
though it’s likely that a more complex example would benefit from the Stream API use, whereas the use of Optional is unlikely to get better with more complex operations. A chain of Optional operations can simplify the code if all absent values or filter mismatches within the chain are supposed to be handled the same way at the end of the chain. If, however, like in your example (and most real life scenarios) every absent value should get a different treatment or be reported individually, using Optional, especially the nested use of Optionals, does not improve the code.

Scala: Check for null for List

I am new to Scala after coding for 10 years in Java. Still getting hold of functional programming.
How can I check if the list is null or not?
Code looks something like this:
val filterList = filters.map { filter =>
//some operations
}
//Other function
filterList.foldLeft(true)((result1, result2) => {
Now if filters is null then filterList is going to be null too.
If filters is null (which is different from being empty) then that indicates some pretty careless programing, but it can be handled.
val filterList = Option(filters).map(_.map { ...
Now filterList is of type Option[X] where X is the collection type for filters. Note the 1st map is to unwrap the Option and the 2nd map maps over the collection, except if filters was null, then the 2nd map is never invoked and the whole result is None.
val filterList = if(filters == null) Seq.empty[SomeType] else filters.map {...}
However, you should try to make sure it never gets to be null, because we try to avoid null variables in Scala. Use the Option[T] type, or empty collections instead

How to iterate nested for loops referring to parent elements using Java 8 streams?

I want to iterate nested lists using java8 streams, and extract some results of the lists on first match.
Unfortunately I have to also get a values from the parent content if a child element matches the filter.
How could I do this?
java7
Result result = new Result();
//find first match and pupulate the result object.
for (FirstNode first : response.getFirstNodes()) {
for (SndNode snd : first.getSndNodes()) {
if (snd.isValid()) {
result.setKey(first.getKey());
result.setContent(snd.getContent());
return;
}
}
}
java8
response.getFirstNodes().stream()
.flatMap(first -> first.getSndNodes())
.filter(snd -> snd.isValid())
.findFirst()
.ifPresent(???); //cannot access snd.getContent() here
When you need both values and want to use flatMap (as required when you want to perform a short-circuit operation like findFirst), you have to map to an object holding both values
response.getFirstNodes().stream()
.flatMap(first->first.getSndNodes().stream()
.map(snd->new AbstractMap.SimpleImmutableEntry<>(first, snd)))
.filter(e->e.getValue().isValid())
.findFirst().ifPresent(e-> {
result.setKey(e.getKey().getKey());
result.setContent(e.getValue().getContent());
});
In order to use standard classes only, I use a Map.Entry as Pair type whereas a real Pair type might look more concise.
In this specific use case, you can move the filter operation to the inner stream
response.getFirstNodes().stream()
.flatMap(first->first.getSndNodes().stream()
.filter(snd->snd.isValid())
.map(snd->new AbstractMap.SimpleImmutableEntry<>(first, snd)))
.findFirst().ifPresent(e-> {
result.setKey(e.getKey().getKey());
result.setContent(e.getValue().getContent());
});
which has the neat effect that only for the one matching item, a Map.Entry instance will be created (well, should as the current implementation is not as lazy as it should but even then it will still create lesser objects than with the first variant).
It should be like this:
Edit: Thanks Holger for pointing out that the code won't stop at the first valid FirstNode
response.getFirstNodes().stream()
.filter(it -> {it.getSndNodes().stream().filter(SndNode::isValid).findFirst(); return true;})
.findFirst()
.ifPresent(first -> first.getSndNodes().stream().filter(SndNode::isValid).findFirst().ifPresent(snd -> {
result.setKey(first.getKey());
result.setContent(snd.getContent());
}));
A test can be found here

Categories