Introduction to Lambdas in JDK 8

We write lambdas essentially as a block of code and a parameter list. The JVM will make an object out of those for us - an object whose target type that will be a functional interface. It's similar to creating an anonymous class, but removes a lot of the boiler plate: it's syntactic sugar for creating instances of functional interfaces.

Syntax of a Lambda

Below is an example lambda being assigned to an instance of Runnable.

Runnable run = () -> System.out.println("Lambra assigned to a Runnable.");

From left to right:

  1. Runnable run =
    • The lambda is being assigned to an instance of Runnable. More on that later.
  2. ()
    • This is the lambda's parameter list (empty here). Parentheses may or may not be required. Types may or may not be required. For example:
      • x
        • Parentheses not needed because we have only parameter.
        • Parameter type not needed because it is inferred from the functional interface.
      • (double x)
        • Need the parentheses because we included the type.
      • ()
        • Parentheses required because we have no parameters!
      • (x, y)
        • Parentheses required because we have more than one parameter.
        • Again, parameter types are inferred from the functional interface.
  3. ->
    • What follows this is the lambda code itself.
  4. { ... }
    • Actual code to be executed when the lambda is run. Could be any of these:
      • An anonymous code block (zero or more statements enclosed in curly braces). For example:
        • Empty block: { }
        • Just one statement:
          Runnable run = () -> {
             System.out.println("Hello World!");
          };
          
        • Multiple statements:
          Runnable run = () -> {
             System.out.println("Hello World!");
             System.out.println("Hello World!");
          };
          
      • A single statement (curly braces optional).
        Runnable run = () -> {
           System.out.println("Hello World!");
        };
        
        or
        Runnable run = () -> System.out.println("Hello World!");
        
      • An expression (no curly braces).
        BinaryCalculator division = (v1, v2) -> v1 / v2;
        

Target type is a functional interface

Lambdas are objects in Java, but we do not have to explicitly define their type (like we do with an anonymous class). Instead, Java will try to match a lambda to a target type. The target type of a lambda is a functional interface. A functional interface is an interface that has only one abstract method defined within it (JDK 8 now allows interfaces to contain static and default methods, but these don't count here). Optionally, a functional interface may be marked with the @FunctionalInterface annotation. For example: Runnable or Callable in JDK 8 are both annotated with @FunctionalInterface.

Lambdas can be passed directly to constructors or methods and the compiler will automagically work out which functional interface to use as a compatible type for the lambda. The compiler will take the parameter list of the lambda and the return type of the lambda code and look for a functional interface whose single method matches it. Consider this example:

Runnable run = () -> System.out.println("Hello World!");

The parameter list of the lambda is empty - (). The return type of the code block is null - System.out.println("Hello World!"). Plus, we are assigning this lambda to an instance of a Runnable, whose sole method (run) accepts no paramaters and has a void return type, so this works.

The interface does not have to marked with the @FunctionalInterface annotation though. This also works.

System.out.println("bbb compared to aaa: "
      + compareStrings((value) -> "bbb".compareTo(value), "aaa"));
// ..
static int compareStrings(final Comparable comparator, final String value1) {
   return comparator.compareTo(value1);
}

The lambda (value) -> "bbb".compareTo(value), "aaa") is the first parameter to the compareStrings method, whose type is a Comparable. The only method to that interface is compareTo - which accepts some type T and returns an int. This matches the lambda - which accepts an object of any type (T is unspecified, so it just has to be some type) and it returns an int. Therefore, the lambda can be assigned to a Comparable instance and the Comparable interface is not marked with the @FunctionalInterface annotation.

Using lambdas

Here is an example of using lambdas, which I have adapted from this brilliant JavaWorld article: The essential Java language features tour, Part 6 - Getting started with lambdas and functional interfaces.

public final class LambdaTest {

   public static void main(final String[] args) {
      final BinaryCalculator addition = (double v1, double v2) -> {
         return v1 + v2;
      };
      final BinaryCalculator division = (v1, v2) -> v1 / v2;
      final UnaryCalculator negation = v -> -v;
      final UnaryCalculator square = (double v) -> v * v;
      final double value1 = 18;
      final double value2 = 36.5;
      System.out.printf("%2.1f + %2.1f = %10.3f%n", value1, value2,
            calculate(addition, value1, value2));

      System.out.printf("%2.1f / %2.1f = %10.3f%n", value1, value2,
            calculate(division, value1, value2));

      System.out.printf("%2.1f / %2.1f = %10.3f%n", value1, value2,
            calculate(negation, value1));

      System.out.printf("%2.1f / %2.1f = %10.3f%n", value1, value2,
            calculate(square, value1));

   }

   static double calculate(final BinaryCalculator calculator,
         final double value1, final double value2) {
      return calculator.calculate(value1, value2);
   }

   static double calculate(final UnaryCalculator calculator,
         final double value) {
      return calculator.calculate(value);
   }

   @FunctionalInterface
   interface BinaryCalculator {
      double calculate(double value1, double value2);
   }

   @FunctionalInterface
   interface UnaryCalculator {
      double calculate(double value);
   }

}

A little bit of discussion on these follows.

final BinaryCalculator addition = (double v1, double v2) -> {
   return v1 + v2;
};

The example above needs parenthesis around the parameters because there are more than one. Parameter types are specified. The body of the lambda could be just an expression, but has been turned into a statement with the return keyword and a semi-colon.

final BinaryCalculator division = (v1, v2) -> v1 / v2;

The example above needs parenthesis around the parameters because there are more than one. Parameter types are left out, because they can be inferred. The lambda is being assigned to an instance of BinaryCalculator: a functional interface whose sole method accepts two parameters of type double, so the JVM can infer the parameter types for the lambda. The body of the lambda here is just an expression (so no curly braces and no semi-colon).

Note: an expression is something that evaluates to a single value. A statement forms a complete unit of execution that ends with a semi-colon. A block is a group of zero or more statements between balanced braces and can be used anywhere a single statement is allowed.

final UnaryCalculator negation = v -> -v;

The example above doesn't need parenthesis around the parameters because there is only one. The lambda is being assigned to an instance of UnaryCalculator: a functional interface whose sole method accepts one parameter of type double, so the JVM can infer the parameter type for the lambda. The body of the lambda here is just an expression (so no curly braces and no semi-colon).

final UnaryCalculator square = (double v) -> v * v;

The above example shows the same things as the one above it except for one thing: even with just one parameter, you can still define the type and surround it with parenthesis.

Type inference is powerful

This deserves a little further explanation. Type inference is syntactic sugar that means we can write shorter code, leaving out a lot of boilerplate code because the compiler will figure out types without us having to explicitly declare them: that's interface type and parameter types. In the example above, I directly assign lambdas to instance variables i.e. the type of the lambda is explicitly declared. Note that the parameter types are still being inferred.

final BinaryCalculator division = (v1, v2) -> v1 / v2;

Now let's look at an example where the lambda is sent directly as an argument, without being declared as a variable first.

System.out.printf("%2.1f + %2.1f = %10.3f%n", value1, value2,
        calculate((v1, v2) -> v1 / v2, value1, value2));

Nothing in the code explicitly says what type the lambda or parameters are and they do not match any local or instance variables. Here we are forcing the compiler to first work out what the target type of the lambda is, and then it has to figure out what parameter types are.

  1. What is the target type? The compiler has to find a functional interface whose sole abstract method matches the parameter list and return type of the lambda, but the compiler won't know the parameter types straight away.
    1. The biggest clue that the compiler can take is by looking at what we are sending the lambda to. We are calling a method called calculate with three parameters. That matches the version of calculate whose first parameter is a BinaryCalculator.
    2. Another clue is that the lambda accepts two parameters.
    3. Maybe the compiler even looks at the body and sees a divide operation that can only return double. (Not sure about this though.)
    4. Then the compiler should look through all the functional interfaces it knows about until it finds one that matches all these conditions:
      1. The functional interface type is assignable to the parameter of the method that the lambda is being sent to.
      2. The functional interface's sole abstract method has a parameter list that matches what it knows about the parameters being sent to the lambda.
      3. The functional interface's sole abstract method has a return type that matches what is being returned by the lambda code.
  2. So it picks BinaryCalculator as the functional interface - it has only one abstract method.
  3. BinaryCalculator's sole abstract method is calculate(double value1, double value2), which accepts two double parameters, so it knows what types to give the parameters too.

Popular Posts