Search code examples
javajava-stream

Reduce a stream to single object without multiple object creation and explicit mutation


I want to create an aggregate object after streaming a collection of incoming object. But what I want is to not create a reference outside the stream and mutate that object "explicitly".

Let's take this scenario. I get a list of students. I want to stream the list and collect only certain elements and form a aggregate object named MySchool

public record Student(String name, String section, String schoolName) {
}

List<Student> students =

                List.of(
                        new Student("John", "2", "Holy school"),
                        new Student("Raghav", "8", "California school"),
                        new Student("Prem", "5", "Holy school"),
                        new Student("Peter", "7", "California school"),
                        new Student("Akbar", "9", "Arun school"),
                        new Student("Ashok", "6", "Arun school"),
                        new Student("Arun", "4", "Arun school"),
                        new Student("Ram", "2", "Arun school"),
                        new Student("James", "2", "California school")
                );

public class MySchool {

    private List<Student> students = new ArrayList<Student>();

    private int studentCount = 0;

    public MySchool addStudent(Student student) {
        this.students.add(student);
        return this;
    }

    public List<Student> getStudents () {
        return this.students;
    }

    public void addStudentCount() {
        this.studentCount++;
    }

    public int getStudentCount() {
        return this.studentCount;
    }

    
}

I don't want to do like below, where I mutate an external reference

public void getMySchoolData(List<Student> allStudents) {

        MySchool mySchool = new MySchool();

        allStudents.stream()
                .filter(student -> student.schoolName().equals("Arun school"))
                .forEach(student -> {
                    mySchool.addStudent(student);
                    mySchool.addStudentCount();
                });


        System.out.println(mySchool);

To avoid doing like above, I used reduce() method of streams, but I ended up creating lot of MySchool objects

public void getMySchoolData1(List<Student> allStudents) {
        Optional<MySchool> mySchool = allStudents.stream()
                .filter(student -> student.schoolName().equals("Arun school"))
                .map(this::addStudentsToSchool)
                .reduce((mySchool1, mySchool2) -> {
                    mySchool1.combineMySchool(mySchool2);
                    return mySchool1;
                });

        var sss = mySchool.get();

        System.out.println(sss);

    }


private MySchool addStudentsToSchool(Student student) {

        var mySchool = new MySchool();
        mySchool.addStudent(student);
        mySchool.addStudentCount();

        return mySchool;
    }

// added this method to MySchool class

public void combineMySchool (MySchool otherSchoolObject) {
        this.studentCount+= otherSchoolObject.getStudentCount();
        this.students.addAll(otherSchoolObject.getStudents());

    }

As you can clearly see, I am able to successfully avoid external mutation after using reduce(), but I end up creating multiple objects and finally combine them and reduce

Is there anyway where I can avoid multiple object creation, rather just create one object and have streams fill that for me and give back ?


Solution

  • I was able to achieve this by using Collectors.teeing() API

    MySchool mySchool = allStudents.stream()
                    .filter(student -> student.schoolName().equals("Arun school"))
                    .collect(
                            Collectors.teeing(
                                    Collectors.toList(),
                                    Collectors.counting(),
                                    mySchool()
                            ));
    

    And this is my Merger Bi-Function

    private BiFunction<List<Student>, Long, MySchool> mySchool() {
    
            return (students, count) -> {
                var mySchool = new MySchool();
                mySchool.addAllStudents(students);
                mySchool.addStudentCount(count.intValue());
                return mySchool;
            };
        }
    

    Teeing is where I get two inputs and giving a merger function will merge and give back the desired result.

    Absolutely excellent !!