CurvedHalo
CurvedHalo

Reputation: 23

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 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

Answers (5)

dani-vta
dani-vta

Reputation: 6840

You could stream your List<Foo> and then:

  1. filter() each foo whose type doesn't match fooType.
  2. map() each foo to itself, while removing with removeIf() all the nested elements that don't meet the required conditions.
  3. Finally, 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

Adriaan Koster
Adriaan Koster

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

Ravi Gupta
Ravi Gupta

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

WJS
WJS

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

Ovidijus Parsiunas
Ovidijus Parsiunas

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

Related Questions