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