Java 8 - Lambda Tutorial

I've been working with Java 8 for a few months now and to be honest I've been pretty impressed so far. There are a number of exciting new features that have changed the language for the better, but lambda expressions, in my opinion stand out.

What is a lambda expression?

In simple terms a lambda expression is a piece of code that can be passed to another method as a parameter, and then executed by the receiving method. The ability to parametrise behaviour is not new to Java and is something you've probably done many times with anonymous classes. Lambdas however provide a more elegant means of passing code to another method for execution. To show you the benefits of lambdas we'll first look at how we would have implemented behaviour parametrisation prior to Java 8.

Problem Domain

Our problem domain is a simple one. We have a list of bank account objects that we'd like to filter using various criteria. The account object is a simple POJO defined as follows (getters & setters have been removed for brevity).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package com.blog.sample.lambdas.model;

public class Account {

    private long accountNumber;
    private AccountType accountType;
    private String accountName;
    private long accountBalance;
    private AccountStatus accountStatus;

    public Account(String accountName, long accountBalance, long accountNumber,
                   AccountStatus accountStatus, AccountType accountType){
  
        this.accountName = accountName;  
        this.accountBalance = accountBalance;  
        this.accountNumber = accountNumber;
        this.accountStatus = accountStatus;
        this.accountType = accountType;
    }
Fig 1.0

We also have a simple AccountProvider class that creates a bunch of Account objects and returns them in an ArrayList.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    public class AccountProvider {

        public static List<Account> getAccountData(){
  
            Account acc1 = new Account("Johns Account", 1200, 12345678, AccountStatus.ACTIVE, AccountType.CURRENT);
            Account acc2 = new Account("Joes Account",1500, 43567896, AccountStatus.ACTIVE, AccountType.CURRENT);  
            Account acc3 = new Account("Marks Account", 1800, 938475633, AccountStatus.ACTIVE, AccountType.CURRENT);
            Account acc4 = new Account("Sarahs Account", 2300, 88374033, AccountStatus.ACTIVE, AccountType.CURRENT);  
            Account acc5 = new Account("Sarahs Savings Account", 6700, 43246875, AccountStatus.ACTIVE, AccountType.SAVINGS);  
            Account acc6 = new Account("MyCo Business Account", 3600, 432432422, AccountStatus.ACTIVE, AccountType.BUSINESS_RESERVE); 
            return Arrays.asList(acc1, acc2, acc3, acc4, acc5, acc6);
        } 
    }
                                                                                               Fig 1.1

Next we'll create our first account filtering method. As you can see it simply iterates over the list and collects Account objects where account type is Current.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    private static List<Account> findCurrentAccounts(List<Account> accounts){
  
        List<Account> matchingAccounts = new ArrayList<>();
        for(Account account : accounts){   
   
           if(account.getAccountType().equals(AccountType.CURRENT)){
              matchingAccounts.add(account);
           }
        }
        return matchingAccounts;
    }
Fig 1.2

The above method is fine but what if we want to filter by some other criteria like AccountStatus? We'd end up with the method shown below, and as you can see it's almost identical to the one we just defined. Both methods iterate through the items in the list, apply a boolean expression and if that expression is satisfied adds the current item to a new list. The only difference between theses 2 methods is the boolean expression that's being applied.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    private static List<Account> findActiveAccounts(List<Account> accounts){
  
        List<Account> matchingAccounts = new ArrayList<>();
        for(Account account : accounts){   
   
            if(account.getAccountStatus().equals(AccountStatus.ACTIVE)){
                matchingAccounts.add(account);
            }
        }
        return matchingAccounts;
    }
Fig 1.3

This approach for filtering Account objects isn't very flexible as we'll likely end up with lots of duplicate code in each of our filter methods.

Pre Java 8 Approach

Before we look at how lambdas can help, lets take a look at how we could have implemented a more flexible solution prior to Java 8 and lambdas. 
We already know that both methods above only differ by the boolean expression used to filter the Accounts. With this in mind we need to create a method that contains the generic code (iteration and application of boolean expression) but also takes the boolean expression as a parameter. To do this we can create an interface with a single method signature that takes generic type T and returns a boolean. I've defined such an interface called MyPredicate as follows.

1
2
3
4
    public interface MyPredicate<T> {
 
       public boolean test(T t);
    }
Fig 1.4

Next we need a generic version of the filter methods we created earlier. Instead of using a hard coded boolean expression, the method will take an instance of the MyPredicate interface defined above. Our new generic method is defined as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    private static List<Account> findAccounts(List<Account> accounts, MyPredicate<Account> predicate){
  
       List<Account> matchingAccounts = new ArrayList<>();
       for(Account account : accounts){   
   
          if(predicate.test(account)){
             matchingAccounts.add(account);
          }
       }
       return matchingAccounts;
    }
Fig 1.5

The findAccounts method signature takes a MyPredicate object as well as the list of Accounts to be filtered. On line 6 we invoke the test method on the MyPredicate instance passed as a parameter.
To call the findAccounts method we need to create an object that implements the MyPredicate interface and provide an implementation of the test method. The implementation of the test method is key as it will contain the boolean expression that is executed by the findAccounts method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    private void findActiveAccountsWithPredicate(List<Account> accounts){
  
        findAccounts(accounts, new MyPredicate<Account>() {  
        @Override
        public boolean test(Account t) {
            if(t.getAccountStatus().equals(AccountStatus.ACTIVE)) return true;    
             return false;
            }
        });
    }
Fig 1.6

On line 3 we call the generic findAccounts method and pass in the list of accounts and an instance of MyPredicate. The MyPredicate instance is passed as an anonymous inner class, which simply means creating a new instance on the fly and immediately providing an inline implementation of the test method.  The test method defined on lines 5 to 7 provides the filtering criteria as a boolean expression for the findAccounts method. This is an example of behaviour parametrisation we discussed earlier, as we've just passed a piece of code as an argument to another method.

So what's the problem?

We've shown how behaviour parametrisation can be achieved prior to Java 8, so what's the problem? In order to pass a simple boolean expression we were forced to create a new object and then provide an implementation for its single test method. This results in a lot of boilerplate code while all we want to do is pass the boolean expression defined on line 6 above. This is where lambdas come to the rescue, as they provide a means of passing just the code required, without the verbosity of an anonymous inner class.

Lambda Solution

We're now going to look at how we can solve the same problem using lambdas. We'll start off by creating a slightly different version of the interface we created earlier.

1
2
3
4
5
    @FunctionalInterface
    public interface Predicate<T> {
 
       public boolean test(T t);
    }
Fig 1.7

The only difference is the addition of the @FunctionalInterface annotation to mark this as a functional interface. In Java 8 a functional interface is defined as having exactly 1 abstract method, and an implementation of a functional interface can be created using a lambda expression. Note that instances can also be created using method references and constructor references but these are beyond the scope of this post.

I've updated the findAccounts method we defined earlier so that it now uses our new Predicate interface (line 6 below).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    private static List<Account> findAccounts(List<Account> accounts, Predicate<Account> predicate){
  
       List<Account> matchingAccounts = new ArrayList<>();
       for(Account account : accounts){   
   
          if(predicate.test(account)){
             matchingAccounts.add(account);
          }
       }
       return matchingAccounts;
    }
Fig 1.8

We now have all the building blocks in place to start writing lambdas. Lets begin by rewriting the filter method from figure 1.6 above. With the original approach we had to create an instance of MyPredicate as an anonymous inner class and provide an implementation of the test method. Lets take a look at the equivalent method using a lambda.

1
2
3
4
    private void findActiveAccountsWithPredicate(List<Account> accounts){
  
       findAccounts(accounts, account -> account.getAccountStatus().equals(AccountStatus.ACTIVE));  
    }
Fig 1.9

The anonymous inner class and test method implementation have been replaced by a simple lambda that defines the boolean expression we want to pass to the findAccounts method. This is much more elegant than our original attempt as we now parametrise behaviour in a concise manner, by passing only the required code.

Lambda Syntax

Lets take a closer look at lambda syntax and some useful rules you should remember. The diagram below describes the basic structure of a lambda.

Fig 2.0
  • A lambda can have zero or more parameters which are typically enclosed in parenthesis.  
  • Parameter types are inferred so don't have to be explicitly defined, although you can explicitly define them if you like.
  • Parameters are typically enclosed in parenthesis and are comma separated like ordinary Java methods.
  • Parenthesis can be omitted when the lambda has a single parameter and that parameter type is inferred. If the parameter type is explicitly defined then parenthesis are required. 
  • Empty parenthesis are used to depict an empty set of parameters 
  • An arrow symbol is required between the parameters and lambda body
  • The body is defined as either an expression or a statement block.
  • An expression can be defined without braces. A statement must be enclosed inside braces.
Below are some simple examples.

1
2
3
4
5
    account -> account.getAccountStatus().equals(AccountStatus.ACTIVE);

    (int a, int b) -> {  return a * b; };
     
    (String s) -> { System.out.println(s); };

Fig 2.1

Using Lambdas with Java 8

At this point we've looked at lambdas and how they offer an elegant means of passing code to another method for execution. We looked at how you can create a FunctionalInterface with a single abstract method and then implement that method using lambdas expressions instead of an anonymous class. While you're free to write your own functional interfaces, the good news is that Java 8 comes with quite a few out of the box. In fact the Predicate Functional Interface I defined in figure 1.7 is actually provided by Java 8 out of the box. I borrowed it to demonstrate a common use case for lambdas.
The official Java documentation details all Functional Interfaces that are provided out of the box. http://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html. I'd recommend taking some time to familiarise yourself with these.

There are lots of great new features in Java 8 that make use of lambdas, most notably the new Stream API which I'll be covering in another post. There are however many existing interfaces in the Java API that have single method interfaces and therefore make ideal candidates for use with lambdas. We're going to look at the Runnable and Comparator interfaces, both of which you're probably already familiar with. I'll show you how you can put lambdas to work right away with these interfaces. This will allow you to harness the power of lambdas right away, before becoming familiar with some of the other new features in Java 8.

Runnable Interface

We'll take a look at the old and new style approaches, so that we can compare how we use the Runnable interface in Java 7, and then the new style approach with Java 8 and lambdas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    private void runnableExample() {

       /* Anonymous Runnable */
       Runnable anonymousRunnable = new Runnable() {
   
          @Override
          public void run() {    
             System.out.println("Executing anonymous runnable...");
          }
       };

       /* Lambda Runnable */
       Runnable lambdaRunnable = () -> System.out.println("Executing lambda runnable...");

       /* run our runnables */
       anonymousRunnable.run();
       lambdaRunnable.run();
    }
Fig 2.2
  • Lines 4 to 10 show how we would have traditionally used the Runnable interface. We use an anonymous class and provide an implementation of the run method to perform the actual work.
  • Line 13 uses a lambda expression to provide the implementation of the run method. Note that the lambda expression can be assigned to the Runnable reference as it is essentially a shorthand class implementation of the Runnable interface.

Comparator Interface

If you've been working with Java for a while, the chances are you've used the Comparator interface at some point. It has a compare method that takes two objects of type T and returns a negative, zero, or positive integer. The value returned indicates that the first parameter is less than, equal to, or greater than the second.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    /* Sort Account objects using anonymous class */    
    Collections.sort(accounts, new Comparator<Account>(){
      
        public int compare(Account acc1, Account acc2){
           return Long.valueOf(acc1.getAccountBalance()).compareTo(acc2.getAccountBalance());
       }
    });  
    
    /* Sort Account objects using anonymous class */        
    Collections.sort(accounts, (acc1, acc2) -> Long.valueOf(acc1.getAccountBalance()).compareTo(acc2.getAccountBalance()));
Fig 2.3
  • Lines 2 to 7 show how we would have traditionally used the Comparator interface. We instantiate an instance of the class and provide an implementation of the compare method to perform the comparison based on object attributes.
  • Line 13 uses a lambda expression to provide the same implementation of the compare method in a more concise manner.

Conclusion

In this post we took an introductory look at lambda expressions, one of the most exciting additions to the Java language for quite some time. Lambdas are a huge part of Java 8 but as we demonstrated above, they can be quickly put to work with some familiar interfaces such as Runnable and Comparator. This means that you can start using lambdas almost immediately, without knowledge of other new Java 8 APIs. However I'd encourage you to delve deeper where you'll see the real power of lambdas when used in conjunction with other new features such as the Streams API. 

Comments

Popular posts from this blog

Spring Boot & Amazon Web Services (EC2, RDS & S3)

Spring Web Services Tutorial

Health Checks, Metrics & More with Spring Boot Actuator

Spring JMS Tutorial with ActiveMQ

Axis2 Web Service Client Tutorial

An Introduction to Wiremock

Spring Batch Tutorial

Externalising Spring Configuration

Spring Quartz Tutorial