Java 101: The Next Generation

Java 101: The essential Java language features tour, Part 6

Getting started with lambdas and functional interfaces

Java 101: The Next Generation

Show More
1 2 Page 2
Page 2 of 2

running
running
Found matched file: '.\LambdaDemo.java'.
running
called
Washington
Sydney
Rome
Ottawa
Moscow
London
Jerusalem
Berlin
Owner

Lambdas and scopes

The term scope refers to that part of a program where a name is bound to a particular entity (e.g., a variable). In another part of the program, the name may be bound to another entity. A lambda body doesn't introduce a new scope. Instead, its scope is the enclosing scope.

Lambdas and local variables

A lambda body can define local variables. Because these variables are considered part of the enclosing scope, the compiler will report an error when it detects that the lambda body is redefining a local variable. Listing 5 demonstrates this problem.

Listing 5. LambdaDemo.java (version 5)


public class LambdaDemo
{
   public static void main(String[] args)
   {
      int limit = 10;
      Runnable r = () -> {
                           int limit = 5;
                           for (int i = 0; i < limit; i++)
                              System.out.println(i);
                         };
   }
}

Because limit is already present in the enclosing scope (the main() method), the lambda body's redefinition of limit (int limit = 5;) causes the compiler to report the following error message: error: variable limit is already defined in method main(String[]).

A local variable or parameter that's defined outside a lambda body and referenced from the body must be marked final or considered effectively final (the variable cannot be assigned to after initialization). Attempting to modify an effectively final variable causes the compiler to report an error, as demonstrated in Listing 6.

Listing 6. LambdaDemo.java (version 6)


public class LambdaDemo
{
   public static void main(String[] args)
   {
      int limit = 10;
      Runnable r = () -> {
                           limit = 5;
                           for (int i = 0; i < limit; i++)
                              System.out.println(i);
                         };
   }
}

limit is effectively final. The lambda body's attempt to modify this variable causes the compiler to report an error. It does so because a final/effectively final variable will need to hang around until the lambda executes, which may not happen until long after the code in which the variable was defined returns. Non-final/non-effectively final variables no longer exist.

Lambdas and this and super

Any this or super reference that is used in a lambda body is regarded as being equivalent to its usage in the enclosing scope (because a lambda doesn't introduce a new scope). However, this isn't the case with anonymous classes, which Listing 7 demonstrates.

Listing 7. LambdaDemo.java (version 7)


public class LambdaDemo
{
   public static void main(String[] args)
   {
      new LambdaDemo().doWork();
   }

   public void doWork()
   {
      System.out.printf("this = %s%n", this);
      Runnable r = new Runnable()
                       {
                          @Override
                          public void run()
                          {
                             System.out.printf("this = %s%n", this);
                          }
                       };
      new Thread(r).start();
      new Thread(() -> System.out.printf("this = %s%n", this)).start();
   }
}

Listing 7's main() method instantiates LambdaDemo and invokes the object's doWork() method to output the object's this reference, instantiate an anonymous class that implements Runnable, create a Thread object that executes this runnable when its thread is started, and create another Thread object whose thread executes a lambda when started.

Compile Listing 7 and run the application. You should observe something similar to the following output:


this = LambdaDemo@75b84c92
this = LambdaDemo$1@6857aa8a
this = LambdaDemo@75b84c92

The first line shows LambdaDemo's this reference, the second line shows a different this reference in the new Runnable scope, and the third output line shows the this reference in a lambda context. The third and first lines match because the lambda's scope is the doWork() method; this has the same meaning throughout this method.

Lambdas and exceptions

A lambda body is not allowed to throw more exceptions than are specified in the throws clause of the functional interface method. If a lambda body throws an exception, the functional interface method's throws clause must declare the same exception type or its supertype. Consider Listing 8.

Listing 8. LambdaDemo.java (version 8)

import java.awt.AWTException;

import java.io.IOException;

@FunctionalInterface
interface Work
{
   void doSomething() throws IOException;
}

public class LambdaDemo
{
   public static void main(String[] args) throws AWTException, IOException
   {
      Work work = () -> { throw new IOException(); };
      work.doSomething();
      work = () -> { throw new AWTException(""); };
   }
}

Listing 8 declares a Work functional interface whose doSomething() method is declared to throw java.io.IOException. The main() method assigns a lambda that throws IOException to work, which is okay because IOException is listed in doSomething()'s throws clause.

main() next assigns a lambda that throws java.awt.AWTException to work. However, the compiler doesn't allow this assignment because AWTException isn't part of doSomething()'s throws clause (and is certainly not a subtype of IOException).

Predefined functional interfaces

You might find yourself repeatedly creating similar functional interfaces. For example, you might create a CheckConnection functional interface with a boolean isConnected(Connection c) method and a CheckAccount functional interface with a boolean isPositiveBalance(Account acct) method. This is wasteful.

The previous examples expose the abstract concept of a predicate (a Boolean-valued function). Anticipating such patterns, Oracle provides the java.util.function package of commonly-used functional interfaces. For example, this package's Predicate<T> functional interface can be used in place of CheckConnection and CheckAccount.

Predicate<T> provides a boolean test(T t) method that evaluates this predicate on its argument (t), returning true when t matches the predicate, and returning false otherwise. Notice that test() provides the same kind of parameter list as isConnected() and isPositiveBalance(). Also, notice that they all have the same return type (boolean).

I've created an application that demonstrates Predicate<T>. Listing 9 presents its source code.

Listing 9. LambdaDemo.java (version 9)


import java.util.ArrayList;
import java.util.List;

import java.util.function.Predicate;

class Account
{
   private int id, balance;

   Account(int id, int balance)
   {
      this.balance = balance;
      this.id = id;
   }

   int getBalance()
   {
      return balance;
   }

   int getID()
   {
      return id;
   }

   void print()
   {
      System.out.printf("Account: [%d], Balance: [%d]%n", id, balance);
   }
}

public class LambdaDemo
{
   static List<Account> accounts;

   public static void main(String[] args)
   {
      accounts = new ArrayList<>();
      accounts.add(new Account(1000, 200));
      accounts.add(new Account(2000, -500));
      accounts.add(new Account(3000, 0));
      accounts.add(new Account(4000, -80));
      accounts.add(new Account(5000, 1000));
      // Print all accounts
      printAccounts(account -> true);
      System.out.println();
      // Print all accounts with negative balances.
      printAccounts(account -> account.getBalance() < 0);
      System.out.println();
      // Print all accounts whose id is greater than 2000 and less than
5000.
      printAccounts(account -> account.getID() > 2000 &&
                               account.getID() < 5000);
   }

   static void printAccounts(Predicate<Account> tester)
   {
      for (Account account: accounts)
         if (tester.test(account))
            account.print();
   }
}

Listing 9 creates an array of accounts with positive, zero, and negative balances. It then proceeds to demonstrate Predicate<T> by invoking printAccounts() with lambdas for printing out all accounts, only those accounts with negative balances, and only those accounts whose IDs are greater than 2000 and less than 5000.

Consider lambda expression account -> true. The compiler verifies that the lambda matches Predicate<T>'s boolean test(T) method, which it does -- the lambda presents a single parameter (account) and its body always returns a Boolean value (true). For this lambda, test() is implemented to execute return true;.

Compile Listing 9 and run the application. You should observe the following output:


Account: [1000], Balance: [200]
Account: [2000], Balance: [-500]
Account: [3000], Balance: [0]
Account: [4000], Balance: [-80]
Account: [5000], Balance: [1000]

Account: [2000], Balance: [-500]
Account: [4000], Balance: [-80]

Account: [3000], Balance: [0]
Account: [4000], Balance: [-80]

Predicate<T> is just one of java.util.function's various predefined functional interfaces. Another example is Consumer<T>, which represents an operation that accepts a single argument and returns no result. Unlike Predicate<T>, Consumer<T> is expected to operate via side-effects. In other words, it modifies its argument in some way.

Consumer<T>'s void accept(T t) method executes an operation on its argument (t). When appearing in the context of this functional interface, a lambda must conform to the accept() method's solitary parameter and return type. Listing 10 presents an example that demonstrates Consumer<T> along with Predicate<T>.

Listing 10. LambdaDemo.java (version 10)


import java.util.ArrayList;
import java.util.List;

import java.util.function.Consumer;
import java.util.function.Predicate;

class Account
{
   private int id, balance;

   Account(int id, int balance)
   {
      this.balance = balance;
      this.id = id;
   }

   void deposit(int amount)
   {
      balance += amount;
   }

   int getBalance()
   {
      return balance;
   }

   int getID()
   {
      return id;
   }

   void print()
   {
      System.out.printf("Account: [%d], Balance: [%d]%n", id, balance);
   }
}

public class LambdaDemo
{
   static List<Account> accounts;

   public static void main(String[] args)
   {
      accounts = new ArrayList<>();
      accounts.add(new Account(1000, 200));
      accounts.add(new Account(2000, -500));
      accounts.add(new Account(3000, 0));
      accounts.add(new Account(4000, -80));
      accounts.add(new Account(5000, 1000));
      // Deposit enough money in accounts with negative balances so that
they
      // end up with zero balances (and are no longer overdrawn).
      adjustAccounts(account -> account.getBalance() < 0,
                     account -> account.deposit(-account.getBalance()));
   }

   static void adjustAccounts(Predicate<Account> tester,
                              Consumer<Account> adjuster)
   {
      for (Account account: accounts)
      {
         if (tester.test(account))
         {
            adjuster.accept(account);
            account.print();
         }
      }
   }
}

Listing 10 continues on from the previous example by introducing an adjustAccounts() method that addresses overdrawn accounts by depositing enough money to give them zero balances. adjustAccounts() takes two lambda arguments, which must conform to Predicate<T>'s and Consumer<T>'s abstract method parameter lists and return types.

The compiler determines that the lambda arguments passed to adjustAccounts() are correct. The test() method is implemented to take an Account account parameter and execute return account.getBalance() < 0;. Similarly, accept() is implemented to take the same parameter and execute account.deposit(-account.getBalance());.

Compile Listing 10 and run the application. You should observe the following output:


Account: [2000], Balance: [0]
Account: [4000], Balance: [0]

In conclusion

Lambdas and functional interfaces have done much to simplify Java source code, but they weren't the only features introduced in Java 8. The final article in the Essential Java language features tour will introduce method references, interface default/static methods, and some lesser-known features. I will also briefly discuss some of the language refinements due in Java 9.

1 2 Page 2
Page 2 of 2