Filter a stream with multiple critera until a result is not null with Java 8

Viewed 195

I have an ArrayList of objects called Student. I want to filter this list and return a single Student object only based on hierarchical conditions.

The conditions are as follows:

  • Select the first COMP student.
  • If none are returned, select an ECON student.
  • If none are returned, select a AGRI student.
  • ... etc.

In my design, I have hard coded this - but maybe it can use a sorted Set or List or something.

My current code works, but seems messy, and will grow quite large if more conditions are introduced.

I need help with the Student filterStudent(List<Student> studentList) method. How to do this in fewer lines of code maybe or method chaining. And how to introduce a sorted Set of Enums that can be filtered based on preference (Example, COMP first, if empty, then ECON next, etc. Stop filtering if one is found).

Code:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

class Student {
    private final int stNumber;
    private final String stModule;//...more fields

    public Student(int stNumber, String stModule) {
        this.stNumber = stNumber;
        this.stModule = stModule;
    }

    public int getStNumber() {
        return stNumber;
    }

    public String getStModule() {
        return stModule;
    }

    @Override
    public String toString() {
        return "Student{" +
                "stNumber=" + stNumber +
                ", stModule='" + stModule + '\'' +
                '}';
    }
}

enum Module {
    COMP("Computers"),
    ECON("Economics"),
    AGRI("Agriculture"),
    PHYS("Physics"); //...very large list

    private String moduleName;

    public String getModuleName() {
        return moduleName;
    }

    Module(String moduleName) {
        this.moduleName = moduleName;
    }
}

public class Question {

    public static void main(String[] args) throws Exception {

        //These lists can be any size and contain any information
        List<Student> studentListOne = Arrays.asList(
                new Student(41, "Economics"),
                new Student(45, "Computers")
        );

        List<Student> studentListTwo = Arrays.asList(
                new Student(11, "Physics"),
                new Student(23, "Agriculture"),
                new Student(86, "Physics"),
                new Student(34, "Economics")
        );

        List<Student> studentListThree = new ArrayList<>();

        System.out.println(filterStudent(studentListOne));
        System.out.println(filterStudent(studentListTwo));
        System.out.println(filterStudent(studentListThree)); //exception
    }

    /**
     * Rules are: Choose the first instance of a COMP student.
     * If the list is empty, choose a ECON student.
     * If the list is still empty, choose a AGRI student.
     *
     * @param studentList
     * @return Student
     */
    public static Student filterStudent(List<Student> studentList) throws Exception {
        Optional<Student> selectedStudent;

        selectedStudent = studentList.stream().filter(student -> student.getStModule().equalsIgnoreCase(Module.COMP.getModuleName())).findFirst();
        if (selectedStudent.isPresent()) {
            return selectedStudent.get();
        }

        selectedStudent = studentList.stream().filter(student -> student.getStModule().equalsIgnoreCase(Module.ECON.getModuleName())).findFirst();

        if (selectedStudent.isPresent()) {
            return selectedStudent.get();
        }

        selectedStudent = studentList.stream().filter(student -> student.getStModule().equalsIgnoreCase(Module.AGRI.getModuleName())).findFirst();

        if (selectedStudent.isPresent()) {
            return selectedStudent.get();
        }

        throw new Exception("No student found matching criteria");

    }
}
2 Answers

You can rewrite your current method with a loop:

for (Module mod : Arrays.asList(Module.COMP, Module.ECON, Module.AGRI)) {
  String subj = mod.getModuleName();

  Optional<Student> selectedStudent = studentList.stream().filter(student -> student.getStModule().equalsIgnoreCase(subj)).findFirst();
  if (selectedStudent.isPresent()) {
    return selectedStudent.get();
  }
}
throw new Exception(...);

If you want to avoid iterating the list multiple times, you can declare a Comparator<Student>:

Map<String, Integer> ordering = Map.of(Module.COMP.getModuleName().toLowerCase(), 0, Module.ECON.getModuleName().toLowerCase(), 1, Module.AGRI.getModuleName().toLowerCase(), 2);
ToIntFunction<Student> studentOrdering = student -> ordering.getOrDefault(student.getModuleName().toLowerCase(), ordering.size());
Comparator<Student> comparator = Comparator.nullsLast(Comparator.comparingInt(studentOrdering));

A simple solution would then be to use Stream.min:

Optional<Student> student = studentList.stream().min(comparator);

and this is probably fine. If you want to do it with a more explicit list, you can stop as soon as you find a COMP student:

Student best = null;
for (Student student : studentList) {
  int comp = comparator.compare(best, student);
  if (comp > 0) {
    best = student;
    int studentOrder = studentOrdering.apply(student);
    if (studentOrder == 0) {
      // Can't find a better student, so fast break.
      break;
    }
  }
}
if (best != null) {
  return best;
}
throw new Exception(...);

One thing you can do to support a dynamic list and order of selected modules is to filter the list, sort it and then pick the first element:

public static Student filterStudent(List<Student> studentList, List<String> moduleOrder)
        throws Exception {
    return studentList.stream().filter(s -> moduleOrder.contains(s.getStModule()))
        .min(Comparator.comparingInt(student -> moduleOrder.indexOf(student.getSModule())))
        .orElseThrow(() -> new Exception("No student found matching criteria"));
}

And to execute your current logic, you can call it with the student list and something like List.of(Module.COMP.getModuleName(), Module.ECON.getModuleName(), Module.AGRI.getModuleName()) as second argument.

This will avoid the repeated iterations over the list. But I'm assuming that the moduleOrder list is small, with a handful of elements as in your example (it is searched a few times for filtering and for sorting)

Related