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

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

Related

How can you access elements of a previous step when using Java Stream API?

I tried to write the following code using the stream API, but found that being not able to access the Parent objects in the stream of Grandchild objects, it is way easier to use classic for-loops. How could a solution using the stream API look like? And are there any advantages to such a solution?
The task is to find certain elements in a structure like this: parent -> List -> List and to get for each matching Grandchildren a String of nameOfParent.nameOfGrandchild.
Simple, working code with for-loops:
List<String> grandchilds = new ArrayList<>();
for (Parent parent : parents) {
String parentName = parent.getName() + ".";
for (Child child : parent.getChilds()) {
if (null != child.getTags() && child.getTags().size() == 1
&& SEARCHED_TAG.equals(child.getTags().get(0))) {
for (Grandchild gc : child.getGrandchilds()) {
grandchilds.add(parentName + gc.getName());
}
}
}
}
I was at this point using stream API where i realized that accessing properties of parent is no longer possible when i have the stream of Grandchild.
List<Grandchild> grandchildren = parents.stream()
.flatMap(parent -> parent.getChilds().stream())
.filter(child -> null != child.getTags() && child.getTags().size() == 1 && SEARCHED_TAG.equals(child.getTags().get(0)))
.flatMap(child -> child.getGrandchilds().stream())
.collect(Collectors.toList());
I found some hints like in the following questions, but the resulting code seemed unnecessarily complex and there is the occasional recommendation to use for-loops instead.
Access element of previous step in a stream or pass down element as "parameter" to next step?
Get parent object from stream
How to iterate nested for loops referring to parent elements using Java 8 streams?
Rather than chaining flatMap() calls in a single pipeline, nest one inside the other:
List<String> grandchilds = parents.stream()
.flatMap(parent -> parent.getChilds().stream()
.filter(tagMatch(SEARCHED_TAG))
.flatMap(child -> child.getGrandchilds().stream().map(gc -> parent.getName() + "." + gc.getName())))
.toList();
private static Predicate<Child> tagMatch(String tag) {
return child -> {
List<String> tags = child.getTags();
return tags != null && tags.size() == 1 && tags.get(0).equals(tag);
};
}
I didn't test that and may have missed a parens in there, but that's the gist of it.
As far as advantages go, I don't see any. That aspect is opinion-based, but in my opinion, the for-loops are easier to read and verify, even after factoring out the rather complicated filtering predicate.

Layered filtering using Java Stream API

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

How to skip stream filter if filtered value is null?

I have a list of Java objects. In database some of objects has got field nextSyncDate and some not. What I want to do is to put filter on java stream but only if this field exists and is for example greater than today date. So simplify, I want to get objects which nextSyncDate is greater than today and objects which hasn't got this field (getting NullPointException after get() on this field).
I have tried something like this but it's not working like I want to..
List<MyObjects> objects;
objects.stream()
.filter(obj -> Objects.nonNull(obj.getNextSyncDate()) && obj.getNextSyncDate().before(new Date()))
On the other hand, all objects has got field counter. What I want to do additionally is to set nextSyncDate (for example for tomorrow) for every object which counter is greater than 15. I tried to .map() objects before .filter() but it's also not working.
When creating a stream, you are not directly modifying its source. You need to collect the data. Your filter seems to work as intended. I would personally not recommend having a filtered stream with side effects (do your "counter > 15 then set nextSyncDate" logic somewhere else) but you could do it with another stream operation such as peek (map is for transformation which isn't necessary in your case but could be used):
List<MyObjects> objects = /* omitted */;
List<MyObjects> filteredObjects = objects.stream()
.peek(MyObjects::setNextSycDateWithSomeLogic())
.filter(obj -> Objects.nonNull(obj.getNextSyncDate()) && obj.getNextSyncDate().before(new Date()))
.collect(Collectors.toList());
The filter can include the counter requirement as well:
final Date TODAY = Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant());
final int COUNTER_THRESHOLD = 15;
objects.stream()
.filter(obj -> Objects.nonNull(obj.getNextSyncDate())
&& obj.getNextSyncDate().before(TODAY)
&& obj.getCounter() > COUNTER_THRESHOLD)
The intermediate stream provided by the filter has the objects which need to be updated, so a forEach can be applied:
final Date TODAY = Date.from(LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant());
final Date TOMORROW = Date.from(LocalDate.now().plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
final int COUNTER_THRESHOLD = 15;
objects.stream()
.filter(obj -> Objects.nonNull(obj.getNextSyncDate())
&& obj.getNextSyncDate().before(TODAY)
&& obj.getCounter() > COUNTER_THRESHOLD)
.forEach(obj -> obj.setNextSyncDate(TOMORROW));
Also, consider using LocalDate instead of deprecated Date.

Java 8 stream through multiple layers and concatenate all items at the bottom-most layer

I currently have a multiple layer structure data that is like this:
Industry class has a private field Set<Company> that can be null.
Company class has a private field Set<Division> that can be null.
Division class has a private field Set<Group> that can be null.
Group class has a private field groupName that can be null and is
retrievable with a getter (getGroupName()).
I am trying to stream an instance of Industry all way down to the Group layer and concatenate all the groupName's into one String with "/" in between.
If the this instance of Industry doesn't contain any groupName, return the string "null".
Based on my limited knowledge of Java 8, I am thinking of coding like this:
industry.stream()
.flatmap(industry -> industry.getCompanies().stream())
.filter(Objects::nonNull)
.flatmap(company -> company.getDivisions().stream())
.filter(Objects::nonNull)
.flatmap(division -> division.getGroups().stream())
.map(group -> group.getGroupName)
.collect(Collectors.joining("/")));
This code seems to flawed in someway. Also, I am not sure where to add the statement that if Industry cannot retrieve any groupName, rather than concatenate all groupName into one string simply return a string "null".
What is the proper way to use Java 8 stream in my situation?
Thanks.
Collectors.joining(…) is based on the class StringJoiner. It offers its delimiter, prefix, and suffix features, but unfortunately not the ability to provide the empty value.
To add that feature, we’ll have to re-implement Collectors.joining, which thankfully is not so hard when using StringJoiner.
Change the last line of your stream operation
.collect(Collectors.joining("/"));
to
.filter(Objects::nonNull) // elide all null elements
.collect(()->new StringJoiner("/", "", "").setEmptyValue("null"), // use "null" when empty
StringJoiner::add, StringJoiner::merge).toString();
I understood your question as pretty much anything can be null. In this case you could create your own function to deal with this. I made one as such:
/**
* Creates a stream function for the provided collection function which ignores all null values.
* Will filter out null values passed into the collection function and null values from the resulting stream
* #param collectionFn
* #param <T>
* #param <R>
* #return
*/
public static <T, R> Function<T, Stream<R>> nullSafeMapper(Function<T, Collection<R>> collectionFn) {
return (v) -> Optional.ofNullable(v)
.map(collectionFn)
.map(Collection::stream)
.orElse(Stream.empty())
.filter(Objects::nonNull);
}
Basically its completely null safe, filtering out anything which is null in the input and output. and could be used as such:
industries.stream()
.flatMap(SO46101593.nullSafeMapper(Industry::getCompanies))
.flatMap(SO46101593.nullSafeMapper(Company::getDivisions))
.flatMap(SO46101593.nullSafeMapper(Division::getGroups))
.map(group -> group.getGroupName())
.filter(Objects::nonNull) // filter out null group names
.collect(Collectors.joining("/"));
You could also take that logic and push it down directly into your expression but since it has to be repeated 3 times it gets a bit... verbose and repetitive
Here is an example with null checks:
String s = industries.stream()
.filter( i -> i.getCompanies() != null ).flatMap( i -> i.getCompanies().stream() )
.filter( c -> c != null && c.getDivisions() != null ).flatMap( c -> c.getDivisions().stream() )
.filter( d -> d != null && d.getGroups() != null ).flatMap( d -> d.getGroups().stream() )
.filter( g -> g != null && g.getGroupName() != null ).map( g -> g.getGroupName() )
.collect( Collectors.joining("/") );
You can replace Collectors.joining("/") with Holger's example.
This should do it:
Stream.of(industry)
.map(Industry::getCompanies).filter(Objects::nonNull)
.flatMap(Set::stream)
.map(Company::getDivisions).filter(Objects::nonNull)
.flatMap(Set::stream)
.map(Division::getGroups).filter(Objects::nonNull)
.flatMap(Set::stream)
.map(Group::getGroupName).filter(Objects::nonNull)
.collect(Collectors.collectingAndThen(Collectors.joining("/"),
names -> names.isEmpty() ? "null" : names));
I'm assuming industry.stream() is incorrect, since you say you're working from "an instance of Industry". Instead, I make a Stream<Industry> with one element.
You need to do the null checks on the sets before you try to call stream on them. You're checking whether stream returns null, which is too late.
The final transform from empty result to "null" falls under the concept of the "finisher" function in Collector. Collectors.joining doesn't let you specify a finisher directly, but you can use Collectors.collectingAndThen to add a finisher to any existing Collector.

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