'Filtering lists and nested lists using streams
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 filter a List of Foo's, retaining only those where Foo.type = "fooType".
- Within these retained Foo's, I wish to filter a list of Bar's on Bar.type = "barType", retaining only those which satisfy the given condition.
- I then want to filter the list of NestedAttribute's on NestedAttribute.id = "attributeID", only retaining those which match this condition.
- Within these retained Foo's, I wish to filter a list of Bar's on Bar.type = "barType", retaining only those which satisfy the given condition.
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());
Solution 1:[1]
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());
Solution 2:[2]
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;
}
Solution 3:[3]
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)));
Solution 4:[4]
This is what you're looking for
public static List<Foo> filterList(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());
}
EDIT: Added classes implementation with toString for test print
public class Main {
public static void main(String[] args) {
ArrayList barListAttrs = new ArrayList();
barListAttrs.add(new NestedAttribute("testAttr1"));
barListAttrs.add(new NestedAttribute("id"));
barListAttrs.add(new NestedAttribute("testAttr2"));
ArrayList fooListBars = new ArrayList();
fooListBars.add(new Bar("bar", barListAttrs));
fooListBars.add(new Bar("testBar1", new ArrayList<>()));
List<Foo> listFoo = new ArrayList<>();
listFoo.add(new Foo("testFoo1", new ArrayList<>()));
listFoo.add(new Foo("foo", fooListBars));
for (Foo f : listFoo) {
System.out.println(f);
}
List<Foo> list2 = filterList(listFoo, "foo", "bar", "id");
System.out.println("\n\n---------------- RESULT ----------------\n");
for (Foo f : list2) {
System.out.println(f);
}
}
}
class Foo {
String type;
List<Bar> bars;
public Foo(String type, List<Bar> bars) {
this.type = type;
this.bars = bars;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public List<Bar> getBars() {
return bars;
}
public void setBars(List<Bar> bars) {
this.bars = bars;
}
@Override
public String toString() {
StringBuilder str = new StringBuilder(type);
str.append(" [");
for (Bar b : bars) {
str.append(b.toString());
str.append(" ");
}
str.append("]");
return str.toString();
}
}
class Bar {
String type;
List<NestedAttribute> nestedAttributes;
public Bar(String type, List<NestedAttribute> nestedAttributes) {
this.type = type;
this.nestedAttributes = nestedAttributes;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public List<NestedAttribute> getNestedAttributes() {
return nestedAttributes;
}
public void setNestedAttributes(List<NestedAttribute> nestedAttributes) {
this.nestedAttributes = nestedAttributes;
}
@Override
public String toString() {
StringBuilder str = new StringBuilder(type);
str.append(" [");
for (NestedAttribute na : nestedAttributes) {
str.append(na.toString());
str.append(" ");
}
str.append("]");
return str.toString();
}
}
class NestedAttribute {
String id;
public NestedAttribute(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public String toString() {
return id;
}
}
Solution 5:[5]
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.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | |
| Solution 3 | Ravi Gupta |
| Solution 4 | |
| Solution 5 |
