1

I have a to filter on some nested Lists. Consider a List<A> which contains List<B> which contains List<C>. I need to filter, to return all the A Objects which contain at least one B which satisfies a given condition, and at least one C which satisfies a given condition.

I've got a contrived example below, which basically is doing what I've tried to implement in my actual example. Take a list of schools in a city Each school has many subjects, and each subject has many teachers. I want to retain a list of schools which has subject.subjectName = 'Math' and at least one teacher within this subject where teacher.age > 65.

I have implemented this using a custom predicate, so I've also made a this in my example. I'm getting an error where 'cannot convert from boolean to Stream' but honestly, I'm struggling in this scenario.

    @Getter
    @Setter
    class School {
        private String schoolId;
        private List<Subject> classes;
        
    }
        
    @Getter
    @Setter
    class Subject {
        String subjectName;
        List<Teacher> teachers;
    }
    
    @Getter
    @Setter
    class Teacher {
        private String teacherName;
        private Integer age;
    }

    public class TestClass {
        public static void main( String[] args ){
            List<School> schools;
            
            // Get All schools where A mathTeacher is over retirement age
            List<School> schoolsWithRetirementAgeMathTeachers = schools.stream()
                    .filter(school -> null != school.getClasses())
                    .flatMap(school -> {
                        return school.getClasses().stream()
                                .filter(subject -> subject.getSubjectName().equalsIgnoreCase("Math"))
                                .filter(subject -> null != subject.getTeachers())
                                .flatMap(subject -> {
                                    return subject.getTeachers().stream()
                                            .filter(teacher -> teacher != null)
                                            .anyMatch(isRetirementAge);
                                });
                    }).collect(Collectors.toList());
                    
        }
                
        public static Predicate<Teacher> isRetirementAge = teacher -> teacher.getAge() > 65;
    
    }
1
  • Is this C you're filtering for under the B you're filtering for or are they unrelated conditions? Commented Mar 30, 2022 at 1:25

2 Answers 2

5

If you're trying to return the parent objects, you don't want to use flatMap(). You just need to nest some anyMatch() calls:

List<A> filtered = listA.stream()
        .filter(a -> a.getListB()
                .stream()
                .anyMatch(b -> testB(b) && b.getListC()
                        .stream()
                        .anyMatch(c -> testC(c))))
        .collect(Collectors.toList());
Sign up to request clarification or add additional context in comments.

4 Comments

And if I wanted to extend this, instead of only seeking to check the presence of a nested value, I wanted to filter out all values which didnt satisfy the condition? I.e. each A would have a List<B> only having items which met the condition testB(). Likewise, each B would have a List<C> which contained only those C items which met the condition of testC() .. how would I do this? it's essentially combining nested filtering and retaining the filter outside the scope of the stream
Then you would need a map() to create a filtered copy of each element. Unless you're willing to modify the lists in place.
if i am looking to get a filtered copy of each element, but I also do not want to modify the originals, would that mean i would have to create copies of A that have filtered copies of b and filtered copies of c?
@experimentunit1998X This only filters out top-level elements (based on the nested contents). If you want to create filtered copies at multiple levels, maybe add a method to A like A filtered(Predicate<B> p1, Predicate<C> p2) { return new A(bList.stream().filter(p1).map(b -> b.filtered(p2)).toList()); }.
1

Since you are looking for Schools as an output, you do not need to map/flatmap your stream into another object. The filtering capability has to be smart enough to go through the nested collections and confirm if there was anyMatch based on your requirements.

Primarily in the below code, we filter all those schools where a subject by the name "Math" is present and one of the teachers teaching Maths is above the mentioned retirement age.

// Get All schools where a Math teacher is over retirement age
List<School> schoolsWithRetirementAgeMathTeachers = schools.stream()
        .filter(school -> school.getSubjects()
                .stream()
                .anyMatch(subject ->
                        subject.getName().equalsIgnoreCase("Math") &&
                                subject.getTeachers()
                                        .stream()
                                        .anyMatch(isRetirementAge)
                ))
        .collect(Collectors.toList());

2 Comments

And if I wanted to extend this, to retain only the instances in the 'Subjects' list where the name = math, i.e. I want to filter out these.. as currently it's only returning the schools with a match, but all the subjects underneath are still present
to get the subjects of a school, then you would have to map your stream

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.