0

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.

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

6
  • Have you tried this using streams? You need listOfFoos.stream.filter(...). What's the output? The listOfFoos? Commented Mar 31, 2022 at 12:12
  • I've tried streams, and yes returning a listOfFoos, but im getting those foo's which match a given scenario, but not removing the nested values which dont match it Commented Mar 31, 2022 at 12:15
  • Then just invert the expression of filter. Commented Mar 31, 2022 at 12:17
  • Why would a Foo instance be anything but a fooType? Same for Bar. It doesn't make sense to me that you need to check Foo's to make certain they're a fooType. It would make more sense (at least to me) that a Foo instance can hold many type and you want to ensure it is a bar type. Commented Mar 31, 2022 at 14:19
  • it's an arbitrary example, im hoping to filter on a given parameter for a given value.. I could swap 'type' for 'id' for example Commented Mar 31, 2022 at 14:23

5 Answers 5

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

Sign up to request clarification or add additional context in comments.

Comments

1

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

Comments

1

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

Comments

1

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.

Comments

1

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.