Mastering Lambda Expressions in Java 8

Lambda expressions were introduced in Java 8. A java lambda expression is basically a function which can be created without belonging to any class. In this post we're going to look what are lambda expressions and how to use it. This is going to be a hands-on article and all the java code in this post will compile fine. So, if you want to follow along that'll be great.

Before we dive into lambda expression, let's create a class Employee. We will use this class throughout our discussion on java 8.

public class Employee {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    Sex gender;
    String emailAddress;
    int age;

    Employee(){

    }

    Employee(String name, String email){
        this.name = name;
        this.emailAddress = email;
    }

    Employee(String name, Sex gender, String email, int age){
        this.name = name;
        this.gender = gender;
        this.emailAddress = email;
        this.age = age;
    }

    public int getAge(){
        return age;
    }

    public void printHello(){
        System.out.println("Hello from "+ this.name);
    }

    public Sex getGender() { return gender;}

    public String getEmailAddress() { return emailAddress;}

    public String getName() { return name;}

    public static int compareByAge(Employee a, Employee b){
        return Integer.compare(a.getAge(), b.getAge());
    }

    public int compareByName(Employee a, Employee b){
        return a.getName().compareTo(b.getName());
    }
}

Now, let's create an interface TestEmployee and a class CheckEmployeeEligbility that implements the interface and provides the implementation of the test method.

interface TestEmployee {
    boolean test(Employee e);
}

class CheckEmployeeEligbility implements TestEmployee{
    public boolean test(Employee e){
        return e.gender == Employee.Sex.FEMALE && e.getAge() >= 18 && e.getAge() <= 25;
    }
}

Let's create an Employee data that we can use to test our custom functions.

Employee p1 = new Employee("Ally", Employee.Sex.FEMALE, "allbeal@me.com", 45);
Employee p2 = new Employee("Harris", Employee.Sex.MALE, "harrisClam@me.com", 26);
Employee p3 = new Employee("Clay", Employee.Sex.MALE, "claydubs@me.com", 42);
Employee p4 = new Employee("Britain", Employee.Sex.FEMALE, "britaingal@me.com", 23);
List<Employee> employees = new ArrayList<>();
employees.add(p1);
employees.add(p2);
employees.add(p3);
employees.add(p4);

Suppose we want to search employees based on some attribute, say age. Let's write a few functions to exactly do that.

// search for employees that match one characteristic.
public static void printEmployeeOlderThan(List<Employee> employees, int age){
    for(Employee e : employees){
        if(e.getAge() >= age) e.printHello();
    }
}

printEmployeeOlderThan(employees, 40);
//output
Hello from Ally
Hello from Clay

// more generalized method
public static void printEmployeesAgeRange(List<Employee> employees, int low, int high){
    for(Employee e : employees){
        if(low <= e.getAge() && e.getAge() < high){
            e.printHello();
        }
    }
}
printEmployeesAgeRange(employees, 10, 30);
//output
Hello from Harris
Hello from Britain

So far so good? Until now we've created data for employees and created a few search functions to find employees based on age criteria.

Now, instead of writing different functions with varied search criteria, we can create a class that implements an interface and provide the implementation of the search function. Let's implement that thought into lines of code:

//printEmployees takes an interface as one of the argument.
public static void printEmployees(List<Employee> employees, TestEmployee tester){
    for(Employee e : employees){
        if(tester.test(e)){
            e.printHello();
        }
    }
}

We have already defined an interface TestEmployee and a class CheckEmployeeEligbility that implements it. Moreover, we've provided the implementation of the test(search) function in the CheckEmployeeEligbility class. So, let's use the printEmployees function

CheckEmployeeEligbility checkEligibility = new CheckEmployeeEligbility();
printEmployees(employees, checkEligibility);

//output
Hello from Britain

This was our search criteria that we implemented in the test function:
e.gender == Employee.Sex.FEMALE && e.getAge() >= 18 && e.getAge() <= 25

That process is totally cumbersome. First we defined an interface, then we created a class that implements it and implemented the test function. Instead of doing all that we can just pass anonymous class as one of the arguments as follows:

printEmployees(employees,new TestEmployee() {
    @Override
    public boolean test(Employee e) {
        return e.gender == Employee.Sex.MALE && e.getAge() >= 18
                && e.getAge() <= 26;
    }
});

This makes our life much easier and we can use different search criteria as needed without writing extra code, which is the dream.

But, you know what? We can make it better. From our code we can deduce that TestEmployee is a functional interface and a functional interface has only one abstract method. So, instead of using anonymous class we can use lambda expressions.

Note:- syntax of lambda expressions are as follows:

(parameters) -> expression
or,
(parameters) -> {statements;}

Let's refactor the above code:

printEmployees(employees, 
    (Employee e) -> e.getGender() == Employee.Sex.MALE && e.getAge() >= 18 && e.getAge() <= 26);

So awesome, right! It's a perfectly good code but we're going to do better, again. JDK already provides several standard functional interfaces. In this case, we are going to use Predicate interface. Predicate is a functional interface whose functional method is test(Object) that returns a boolean.

So, let's refactor printEmployees to use Predicate as an argument.

public static void printEmployeesWithPredicate(List<Employee> roster, Predicate<Employee> tester) {
    for(Employee p : roster){
        if(tester.test(p)){
            p.printHello();
        }
    }
}

Now all we need to do is to provide a test function for printEmployeesWithPredicate. For example:

printEmployeesWithPredicate(employees, 
    e -> e.getGender() == Employee.Sex.MALE && e.getAge() >= 18 && e.getAge() <= 26); // test function criteria

Predicate represents a boolean valued function. What if we want to use an interface that returns a void? We can use Consumer\ interface. Consumer functional method is accept(Object) that accepts a single argument and returns no result. Consumer is expected to operate via side-effects. Let's create a function that accepts both Predicate and Consumer and does some operation based on the result.

public static void consumeEmployees(List<Employee> employees, Predicate<Employee> tester, Consumer<Employee> employeeAction){
    for(Employee e : employees){
        if(tester.test(e)){
            employeeAction.accept(e);
        }
    }
}

We can use consumeEmployees as follows:

consumeEmployees(
        employees,
        e -> e.getGender() == Employee.Sex.MALE && e.getAge() >= 18 && e.getAge() <= 26, // test function criteria
        e -> e.printHello() // accept function criteria
);

By now you must have realized that lambdas are pretty cool. At first it can be daunting but once you get it, you realize how simple and easy it is.

Let's look into one more use case. Suppose we want to retrieve some information from Employee data, how do we do it? We will use Function\<T, R> interface. Function accepts one argument and produces a result. Its functional method is apply(Object). Let's write another method that uses all the interfaces that we have discussed so far.

public static void retrieveEmployeeInformation(List<Employee> employees, Predicate<Employee> tester, Function<Employee, String> employeeMap, Consumer<String> employeeAction){
    for(Employee e : employees){
        if(tester.test(e)){
            String data = employeeMap.apply(e);
            employeeAction.accept(data);
        }
    }
}

Let's test the above function:

retrieveEmployeeInformation(
        employees,
        e -> e.getGender() == Employee.Sex.MALE && e.getAge() >= 18 && e.getAge() <= 26, // test function criteria
        e -> e.getEmailAddress(), // accept function criteria
        emailAddress -> System.out.println(emailAddress) // apply function criteria
);

Let's take one step further. We can refactor the above code to use entirely generics. Using generics will help us to reuse the same code with different inputs. The only difference with generics is that inputs to formal parameters are values, while the inputs to type parameters are types.

public static <T, R> void processGenericElements (Iterable<T> source, Predicate<T> tester, Function<T, R> mapper, Consumer <R> action){
    for(T t : source){
        if(tester.test(t)){
            R data = mapper.apply(t);
            action.accept(data);
        }
    }
}

We can also use aggregate operations that accept lambda expressions. Instead of calling retrieveEmployeeInformation we will convert source into stream and apply aggregate operations as follows:

employees.stream().filter(
        e -> e.getGender() == Employee.Sex.FEMALE && e.getAge() >= 18 && e.getAge() <= 26)
        .map(e -> e.getEmailAddress())
        .forEach(emailAddress -> System.out.println(emailAddress));

If you don't know aggregate operations, I'll suggest you to check my blog post on aggregate operations.

If you've made this far then I believe that you have a good grasp of lambda expressions now. I sincerely advice you to use lambdas wherever necessary and also refactor your old code to use them. Moreover, if you plan to write asynchronous programming in java 8 using CompletableFuture API then you're going to save a lot of frustration by using lambda expressions. If you want to learn about asynchronous programming in java 8, you can read my blog post here CompletableFuture java8.

References: lambda expressions javadoc, generics javadoc, functional interfaces javadoc