Reputation: 23
I have to filter a list, based on the value of an attribute. I also have to filter a nested list, based on one of its attributes, and likewise for another nested list. I wondered how this might be possible in a stream.
Example:
I want to return the list of foo's, from this.
void test() {
List<Foo> listOfFoos;
for(Foo foo : listOfFoos) {
if(foo.getType().equalsIgnoreCase("fooType")) {
// If foo matches condition, retain it
for(Bar bar : foo.getBars()) {
if(bar.getType().equalsIgnoreCase("barType")) {
// If Bar matches condition, retain this Bar
for(NestedAttribute attribute : bar.getNestedAttributes()) {
if(attribute.getId().equalsIgnoreCase("attributeID")) {
// retain this attribute and return it.
}
}
} else {
// remove bar from the list
foo.getBars().remove(bar);
}
}
}else {
// remove Foo from list
listOfFoos.remove(foo);
}
}
}
@Getter
@Setter
class Foo {
String type;
List<Bar> bars;
}
@Getter
@Setter
class Bar {
String type;
List<NestedAttribute> nestedAttributes;
}
@Getter
@Setter
class NestedAttribute {
String id;
}
I have tried this:
listOfFoos = listOfFoos.stream()
.filter(foo -> foo.getType().equalsIgnoreCase("fooType"))
.flatMap(foo -> foo.getBars().stream()
.filter(bar -> bar.getType().equalsIgnoreCase("barType"))
.flatMap(bar -> bar.getNestedAttributes().stream()
.filter(nested -> nested.getId().equalsIgnoreCase("attributeID"))
)
).collect(Collectors.toList());
Upvotes: 0
Views: 1523
Reputation: 6840
You could stream your List<Foo>
and then:
filter()
each foo
whose type
doesn't match fooType
.map()
each foo
to itself, while removing with removeIf()
all the nested elements that don't meet the required conditions.collect()
the remaining filtered elements.public static List<Foo> filterList(List<Foo> list, String fooType, String barType, String attrID) {
return list.stream()
.filter(foo -> foo.getType().equalsIgnoreCase(fooType))
.map(foo -> {
foo.getBars().removeIf(bar -> !bar.getType().equalsIgnoreCase(barType));
foo.getBars().forEach(bar -> bar.getNestedAttributes().removeIf(attr -> !attr.getId().equalsIgnoreCase(attrID)));
return foo;
})
.collect(Collectors.toList());
}
Alternatively, you could use the peek()
method to keep each lambda as a single line. However, I do not recommend this approach, since peek()
should be used only for debugging purposes, as the documentation states:
This method exists mainly to support debugging, where you want to see the elements as they flow past a certain point in a pipeline
public static List<Foo> filterList2(List<Foo> list, String fooType, String barType, String attrID) {
return list.stream()
.filter(foo -> foo.getType().equalsIgnoreCase(fooType))
.peek(foo -> foo.getBars().removeIf(bar -> !bar.getType().equalsIgnoreCase(barType)))
.peek(foo -> foo.getBars().forEach(bar -> bar.getNestedAttributes().removeIf(attr -> !attr.getId().equalsIgnoreCase(attrID))))
.collect(Collectors.toList());
}
Upvotes: 1
Reputation: 16209
I assumed you wanted all the "fooType" foos, with only the "barType" bars and "attributeID" nestedAttibutes within.
Then something like:
List<Foo> selected = listOfFoos.stream()
// keep the "footType" foos
.filter(foo -> foo.getType().equalsIgnoreCase("fooType"))
// map each foo to itself
.map(foo -> {
// ... but sneakily remove the non-"barType" bars
foo.getBars().removeIf(bar -> !bar.getType().equalsIgnoreCase("barType"))
return foo;
}
// map each foo to itself again
.map(foo -> {
// iterate over the bars
foo.getBars().forEach(bar ->
// remove the non-"attributeID" nested attributes
bar.getNestedAttributes().removeIf(nested -> !nested.getId().equalsIgnoreCase("attributeID"))
);
return foo;
}
.collect(Collectors.toList());
Note that this is actually modifying the nested collections, instead of just creating a stream. To obtain filtered nested collections would require either doing it like this, or creating new nested collections.
Upvotes: 1
Reputation: 224
You can try this option. It's not fluent statement but three fluent one.
Function<Bar, List<NestedAttribute>> filterAttributes
= bar -> bar.getNestedAttributes()
.stream()
.filter(a -> "attributeId".equals(a.getId()))
.collect(Collectors.toList());
Function<Foo, List<Bar>> filterBarsAndAttributes
= foo -> foo.getBars()
.stream()
.filter(b -> "barType".equals(b.getType()))
.peek(b -> b.setNestedAttributes(filterAttributes.apply(b)))
.collect(Collectors.toList());
listOfFoos.stream()
.forEach(f -> f.setBars(filterBarsAndAttributes.apply(f)));
Upvotes: 1
Reputation: 40034
I am certain you can accomplish this with streams but I don't believe that it lends itself to that very well. The problem is that streams along with map replaces the existing element with a new one, perhaps of different type.
But it is necessary to maintain access to previously constructed types to build the hierarchy. mapMulti
would be a possibility but it could get cluttered (More so than below).
The following creates a new hierarchy without any deletions (removal in a random access list can be expensive since either a linear search is required or a repeated copying of values) and adds those instances which contain the type you want. At each conditional, a new instance is created. At those times, the previous list is updated to reflect the just created instance.
After generating some variable data, this seems to work as I understand the goal.
static List<Foo> test(List<Foo> listOfFoos) {
List<Foo> newFooList = new ArrayList<>();
for (Foo foo : listOfFoos) {
if (foo.getType().equalsIgnoreCase("fooType")) {
Foo newFoo = new Foo(foo.getType(), new ArrayList<>());
newFooList.add(newFoo);
for (Bar bar : foo.getBars()) {
if (bar.getType().equalsIgnoreCase("barType")) {
Bar newBar = new Bar(bar.getType(), new ArrayList<>());
newFoo.getBars.add(newBar);
for (NestedAttribute attribute : bar
.getNestedAttributes()) {
if (attribute.getId().equalsIgnoreCase(
"attributeID")) {
newBar.getNestedAttributes().add(attribute);
}
}
}
}
}
}
return newFooList;
}
Upvotes: 1
Reputation: 2732
You can do this with the stream filter lambda expression, but the resultant cohesion will unfortunately not be great:
listOfFoos.stream()
.filter(foo ->
(foo.getType().equalsIgnoreCase("fooType") && (foo.getBars().stream()
.filter((bar -> (bar.getType().equalsIgnoreCase("barType") && (bar.getNestedAttributes().stream()
.filter(nestedAttribute -> nestedAttribute.getId().equalsIgnoreCase("attributeID"))
).count() > 0)))
).count() > 0))
.collect(Collectors.toList());
Upvotes: 1