Should I use several streams o a single loop?

Imagine we are working with this class:

public Student{
  String name;
  Integer age;
  Integer height;
  Integer weight;
}

Now we’ve got a list of students and we are asked something like:

  • filter those whose name is “Jonh” and get the average age
  • filter those whose name is “Mary” and get the biggest hight
  • filter those whose name is “Ben” and get the smallest weight

I think thar a clean and understandble solution is to use lambdas to filter by name and get what is asked for:

List<Student> students = ...
double jonhAge = students.stream().filter(s->s.getName().equals("Jonh").mapToInt(s->s.getAge()).average()
double maryHeight = students.stream().filter(s->s.getName().equals("Mary").mapToInt(s->s.getHeight()).max()
double benWeight = students.stream().filter(s->s.getName().equals("Ben").mapToInt(s->s.getWeight()).min()

I guess that at least it is iterating over the list 3 times, and using a single loop with conditions might be more efficent:

double jonhAge = 0;
double jonhCount = 0;
double maryHeight = 0;
double benWeight = 1000;
for(Student s : students){
  if(s.getName.equals("Jonh")){
    jonhAge += s.getAge();
    jonhCount++;
  }else if(s.getName.equals("Mary")){
    if(s.getHeight()>maryHeight)
      maryHeight = s.getHeight();
  }else if(s.getName.equals("Ben")){
    if(s.getWeight()<benWeight )
      benWeight = s.getWeight();
  }
}
jonhAge = jonhAge / jonhCount;

I think that the first way is cleaner but the second one is faster. Am I right? Which one shoud I use? Imagine that the list of students contains a huge number of elements, and there are more names than Jonh, Mary and Ben.

Answer

This can be implemented with Stream API in the following way:

  1. Create a map of names to the appropriate getters of Student class
  2. Create another map of names to the getters for IntSummaryStatistics class
  3. Build a map with Collectors.groupingBy + Collectors.summarizingInt to get the stats per student, and transform this map with Collectors.toMap to get required stats parameter by the student.

Update
An enum may be implemented to store references to appropriate getters in Student and IntSummaryStatistics, then the map should use the enum values.

enum FieldStat {
    AGE_AVERAGE(Student::getAge, IntSummaryStatistics::getAverage),
    HEIGHT_MAX(Student::getHeight, IntSummaryStatistics::getMax),
    WEIGHT_MIN(Student::getWeight, IntSummaryStatistics::getMin);

    private final ToIntFunction<Student> field;
    private final Function<IntSummaryStatistics, Number> stat;

    FieldStat(ToIntFunction<Student> getter, Function<IntSummaryStatistics, Number> stat) {
        this.field = getter;
        this.stat = stat;
    }

    public ToIntFunction<Student> getField() {
        return field;
    }

    public Function<IntSummaryStatistics, Number> getStat() {
        return stat;
    }
}

Example implementation:

List<Student> students = Arrays.asList(
    new Student("John", 35, 177, 78), new Student("John", 29, 173, 72),
    new Student("Mary", 32, 164, 58), new Student("Mary", 28, 167, 56),
    new Student("Ben",  24, 181, 84), new Student("Ben",  27, 169, 65),
    new Student("Bob",  35, 178, 80)
);

Map<String, FieldStat> mapStat = Map.of(
        "John", FieldStat.AGE_AVERAGE,
        "Mary", FieldStat.HEIGHT_MAX,
        "Ben",  FieldStat.WEIGHT_MIN
);

Map<String, Number> stats = students.stream()
    .filter(st -> mapStat.containsKey(st.getName()))
    .collect(Collectors.groupingBy(
        Student::getName,
        Collectors.summarizingInt(st -> mapStat.get(st.getName()).getField().applyAsInt(st))
    ))
    .entrySet().stream()
    .collect(Collectors.toMap(
        Map.Entry::getKey, 
        e -> mapStat.get(e.getKey()).getStat().apply(e.getValue())
    ));

System.out.println(stats);

Output

{John=32.0, Ben=65, Mary=167}

Leave a Reply

Your email address will not be published. Required fields are marked *