One of the core features of modern Java is lambda expressions. Introduced in Java 8, lambdas provide concise syntax allowing the deferred execution of a block of code. Put a different way, lambdas allow us to pass behaviour as a method parameter. When the method executes, the lambda expression is run. This capability is often referred to as behaviour parameterization.

Behaviour parameterization can be achieved in a number of ways, of which lambda expressions are usually the most convenient, and they are definitely the most concise. But what is behaviour parameterization, and why would we want to use it? To motivate this discussion, let’s work through a real-world example of filtering a list of items according to some criteria. More concretely, let’s investigate the problem filtering a list of students to find the ones with the best grades.

Filtering by Value

First, we define our Student class with some simple properties.

public class Student {
    private String name;
    private Double gradePointAverage;
    private Integer age;

    // Standard constructor, getter, setter, etc.

Then we can filter the list of students using a simple conditional and loop:

public static List<Student> filterGoodStudents(List<Student> students) {
  List<Student> result = new ArrayList<>();
  for (Student student : students) {
    if (student.getGradePointAverage() >= 3.7) {
      result.add(student);
    }
  }

  return result;
}

The core of this algorithm is the conditional student.getGradePointAverage() >= 3.7. This simple test provides the filtering function and the rest of the algorithm is simple boilerplate around the solution. For example, if we now wish to find great students with a 4.0 GPA we can use the same algorithm, varying only the conditional. The method filterGreatStudents updates the previous method by returning students that have a GPA of 4.0 or higher.

public static List<Student> filterGreatStudents(List<Student> students) {
  List<Student> result = new ArrayList<>();
  for (Student student : students) {
    if (student.getGradePointAverage() >= 4.0) {
      result.add(student);
    }
  }

  return result;
}

Parameterizing our Filter

Naturally, once we have filters in place for finding good and great students, we get a request for an additional function that filters all students with a passing grade. Since we are repeating ourselves three times now, it would be easier to improve our interface by allowing the caller to supply the GPA they wish to filter by. We can add a GPA parameter to our function and change the conditional to student.getGradePointAverage() >= gpa to filter using our parameter.

public static List<Student> filterByGpa(List<Student> students, Double gpa) {
  List<Student> result = new ArrayList<>();
  for (Student student : students) {
    if (student.getGradePointAverage() >= gpa) {
      result.add(student);
    }
  }

  return result;
}

This is our first example of behaviour parameterization. We are allowing the behaviour of our method to be changed based on a parameter. Clearly, this example is very simple, and it breaks down as we get to more complex cases. For example, what if we now have a request to filter students by age? Knowing what we know about parameterization, instead of creating a new hard-coded filter function we jump straight to adding an age parameter to our method and using that in the filter.

public static List<Student> filterByAge(List<Student> students, Integer age)

And maybe another method for filtering by both age and by GPA:

public static List<Student> filterByAgeAndGpa(List<Student> students, Integer age, Double gpa)

And then maybe add a flag to signal which filter to use at a time. When true the byAge flag in this method will filter using the age parameter, and when false the method will filter using the gpa parameter.

public static List<Student> filterByAgeOrGpa(List<Student> students, Integer age, Double gpa, Boolean byAge) {
  List<Student> result = new ArrayList<>();
  for (Student student : students) {
    if (byAge) {
      if (student.getAge() >= age) {
        result.add(student);
      }
    } else {
      if (student.getGradePointAverage() >= gpa) {
        result.add(student);
      }
    }
  }

  return result;
}

Hopefully you can see how this is becoming a messy implementation. To drive the point home, consider the client perspective when trying to use this method. It requires some pretty arcane parameters to make sense of.

List<Student> goodStudents = filterByAgeOrGpa(students, 0, 3.7, false);
List<Student> passingStudents = filterByAgeOrGpa(students, 0, 2.0, false);
List<Student> oldStudents = filterByAgeOrGpa(students, 16, 0.0, true);
List<Student> allStudents = filterByAgeOrGpa(students, 5, 0.0, true);

This just isn’t working very well. What is the problem? We are trying to parameterize the filter algorithm with values like Int and Double. This works fine for certain problems that are very well defined, but in our case it would be much better if could parameterize the behaviour of this method, which is controlled by the conditional that does the filtering.

Filtering by Predicate

In functional programming, a predicate is a function that returns a boolean. A predicate for filtering students can be simply defined with the following interface:

public interface StudentPredicate {
	boolean test(Student s);
}

And we can implement the filtering method to leverage our predicate. The following method takes the predicate as a parameter, and uses the predicates test function as the conditional to filter by.

public static List<Student> filterStudents(List<Student> students, StudentPredicate predicate) {
  List<Student> result = new ArrayList<>();
  for (Student student : students) {
    if (predicate.test(student)) {
      result.add(student);
    }
  }
  return result;
}

Now, any time we want to change the behaviour of our filter, we can supply a new implementation of the StudentPredicate without changing any of the filter implementation.

public class GreatStudentsPredicate implements StudentPredicate {
  @Override
  public boolean test(Student s) {
    return s.getGradePointAverage() >= 3.7;
  }
}

As a client, using this revised filter method is also straightforward:

List<Student> greatStudents = filterStudents(students, new GreatStudentsPredicate());

With this attempt at filtering, the behaviour of the filter method depends on the code passed into it via the predicate object while the logic for iterating through a collection and applying a filtering test remains the same. The downside to this approach is that each new predicate requires the client to create an additional class that implements the predicate interface. This is fairly cumbersome and verbose for the functionality you get in return.

Filtering by Anonymous Class

A feature called anonymous classes can be used by clients of our interface to implement our predicate without needing to create a new class. For example, a caller to our filter function can supply our predicate with the following syntax:

List<Student> greatStudents = StudentFilter.filterStudents(students, new StudentPredicate() {
  @Override
  public boolean test(Student s) {
    return s.getGradePointAverage() >= 3.7;
  }
});

Anonymous classes take us one step closer to easy-to-use behaviour parameterization, but they are still fairly verbose and they can be confusing to use as the functionality of the class becomes more complex.

Filtering with Lambdas

We can further improve our client code by leveraging a Java feature called lambdas that reduce the verbosity of our implementation. The following code uses the sample filtering method, but provides the implementation of the predicate using lambda syntax, which reads as “given a student s, then execute and return the following code”.

List<Student> greatStudents = StudentFilter.filterStudents(students, s -> s.getGradePointAverage() >= 3.7);

With lambdas, we’ve found a concise and flexible implementation of behaviour parameterization. A lambda is a concise representation of an anonymous class that can be passed around to methods. The lambda function is not associated with a named class, but it does have a list of parameters, a body, a return type, and a set of possible exceptions.