Java 8 Lambda Expressions

From NovaOrdis Knowledge Base
Jump to navigation Jump to search

External

Internal

TODO

Overview

Java 8 introduces functional programming features, in the form of lambda expressions. Lambda expressions allow behavior parameterization - functions can be now assigned to variables, as values, and passed around, which essentially means passing code around. Functions come in form of lambda expressions (anonymous functions) or method references.

Lambda Expression

A lambda expression is a representation of an anonymous function: it does not have a name, but it has a list of parameters, a body, a return type, and possibly a list of exceptions that can be thrown.

Lambda expressions can be stored as values, in variables, and passed as arguments to methods and constructors. In all these cases, the type of the lambda value is a functional interface. Lambda expressions let you provide the implementation of the abstract method of the functional interface directly in-line and threat the whole expression as an instance of a concrete implementation of the functional interface. Lambda expression values can be assigned to variables or passed to methods everywhere the corresponding functional interface argument is expected. The compiler will insure the lambda implementation matches the type.

Unlike Scala, which has a function type, Java reuses existing nominal types provided by the functional interfaces and maps them to a form of functional types behind the scenes.

Semantically, lambda expressions do not allow programmers to do anything that couldn't have been done before their introduction in Java 8, via anonymous classes. However, the lambda syntax is more concise, lambda expressions are a convenient way of increasing the code clarity by in-lining logic. If the lambda's body exceeds a few lines in length, so that its behavior isn't instantly clear, the logic should be encapsulated in a method and a method reference should be used instead of the lambda expression.

The term lambda comes from a system developed in academia called lambda calculus, which is used to describe computations.

Lambda Expression Syntax

A lambda expression declaration consists in a comma-separated list of formal parameters, enclosed in parentheses, followed by the "arrow" token, followed by a body.

(comma-separated-parameter-list) -> body

Formal Parameters

The most generic format of the parameter-list is:

([Type1] var1, [Type2] var2, ...)

The type of parameters may be omitted, thanks to the type inference mechanism.

If and only if there is a single parameter, the enclosing parentheses may be omitted, along with the type specification. Note that omitting only the parentheses, but not the type specification is considered a syntax error, both must be omitted at the same time. Example:

a -> ...

Note that sometimes the code is more readable if we include the types explicitly and sometimes more readable if we exclude them.

The parameter list could be empty, but in this case the parentheses are required:

 () -> ...

"->" is referred to as "arrow".

The Body

The body can be a single expression, and it this case the lambda is known as an "expression-style lambda", or a list of statements included enclosed in curly braces - a block - and in this case the lambda is known as "block-style lambda".

Block-Style Lambda

(parameter-list) -> { statement1; statement2; ... }

The absence of a return as the last statement in the block implies that the lambda returns void:

(Apple a) -> { 
    System.out.println("the apple is " + a.getColor());
    System.out.println("the apple weighs " + a.getWeight() + " grams");
};

This lambda, that has no parameters and returns void, is valid:

() -> {}

If the lambda returns a value, the last statement in the block must be return:

(Apple a) -> { 
    boolean heavy = a.getWeight() > 100; 
    return a.getColor() + "(" + (heavy ? "heavy":"light") + ")"; 
};

If the lambda returns void, and the body consists in a single statement that also returns void, the curly braces around the body may be omitted:

(...) -> System.out.println("something");

Special Void-Compatibility Rule

A lambda that returns void may accept a statement whose return is non-void. In this case the return is discarded.

@FunctionalInterface
public interface FunctionalInterfaceWhoseMethodReturnsVoid {

    void update(String s);

}

final List<String> shared = new ArrayList<>();

FunctionalInterfaceWhoseMethodReturnsVoid l = s -> shared.add(s);

Note that shared.add(s) returns a boolean, while update() returns void. The return value of the shared.add(s) statement is discarded.

Expression-Style Lambda

(parameter-list) -> expression

If the lambda is supposed to return a value, then the value the expression evaluates to is returned as value of lambda execution.

(Apple a) -> a.getColor() + " (" + a.getWeight() + " grams)";

If the lambda is supposed to return void, the value the expression evaluates to is discarded.

Functional Interface

A lambda value type is specified by a functional interface: lambda values can be used and passed around everywhere a functional interface is expected.

A functional interface is a Java interface that specifies exactly one abstract method. Note that an interface that has exactly one abstract method and several default methods is still a valid functional interface. In different words, an interface is still a functional interface if it has default methods, as long as it specifies only one abstract method.

We call the abstract method of a functional interface the function descriptor.

Function Descriptor

The function descriptor is the only one abstract method of a functional interface. The function descriptor may also be referred to as functional method.

The function descriptor's method signature - the parameter list, the return type and possibly the exceptions thrown - must match the signature of the lambda expression. The name of the abstract method is irrelevant, all that matters are the parameters, the return type and the thrown exceptions, if any.

Sometimes, the following notation is uses to express the function descriptor:

(parameter-type-list) -> return-type

Example:

() -> void
(Apple, Apple) -> int

@FunctionalInterface

This annotation is used to indicate that an interface is intended to be used as a functional interface, and is therefore useful for documentation. The compiler will return an error if you annotate with @FunctionalInterface an interface that isn't a functional interface. The @FunctionalInterface annotation is not mandatory, but it is a good practice to use it when an interface is designed with this purpose. In this respect, is similar to the @Override annotation.

Library @FunctionalIntefaces

Java 8 comes with a set of pre-defined functional interfaces that describe common function descriptors. Those interfaces are annotated with @FunctionalInterface. More details on "java.util.function" interfaces:

https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html

Runnable

https://docs.oracle.com/javase/8/docs/api/java/lang/Runnable.html

Comparable

java.lang.Comparable<T>

Comparator

java.util.Comparator<T>

Callable

java.util.concurrent.Callable<V>

Predicate, IntPredicate, LongPredicate, DoublePredicate

java.util.function.Predicate<T>

BiPredicate

https://docs.oracle.com/javase/8/docs/api/java/util/function/BiPredicate.html

Function, IntFunction, ToIntFunction

Select/extract from an object.

java.util.function.Function<T, R>

BiFunction

java.util.function.BiFunction<T, U, R>

Consumer

https://docs.oracle.com/javase/10/docs/api/java/util/function/Consumer.html

Represents an operation that accepts a single input argument and returns no result. Unlike most other functional interfaces, Consumer is expected to operate via side-effects.

@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

BiConsumer

https://docs.oracle.com/javase/8/docs/api/java/util/function/BiConsumer.html

Represents an operation that accepts to input arguments and returns no result. Unlike most other functional interfaces, BiConsumer is expected to operate via side-effects.

@FunctionalInterface
public interface BiConsumer<T, U> {
  void accept(T t, U u);
}

A Map.forEach() expects a BiConsumer instance to which it will pass, in order, the key and the value.

Supplier

java.util.function.Supplier<T>

UnaryOperator

java.util.function.UnaryOperator<T>

BinaryOperator

Combine two values.

java.util.function.BinaryOperator<T>

Lambdas and Exceptions

@FunctionalInterface
public interface ExceptionLambda {

    String process(String s) throws Exception;
}

...

ExceptionLambda el = s -> {

  if ("something".equals(s)) {
      throw new Exception("something");
  }

  return s.toUpperCase();
}

If you use a standard functional interface whose signature does not allow exceptions, the lambda implementation will need to catch whatever exceptions are thrown by its statements and wrap them in RuntimeExceptions.

Lambdas and Compile-Time Type Checking

Lambda instances can be assigned to functional interface-typed variables. However, the lambda implementation does not contain any information on the functional interface it is specifically implementing - if any.

The type of a lambda is deduced from the context into which the lambda is used: assignment, method invocation (parameters and return) or cast. The type thus determined is called the target type. The compiler performs a compatibility check, making sure that the signature of the target type's function descriptor is identical with the the lambda's signature.

Because all that needs to match is the signature of the lambda, the same lambda expression can be associated with different functional interfaces, as long as they have the same function descriptor signature.

Lambda Parameters Type Inference

The compiler deduces the target type from context, so it knows the type of the parameters - they are specified in the functional interface definition. Thus, it is not necessary to specify the type of the parameters in the lambda expression declaration: if the types are specified, they are tolerated, but they are not required.

The following declarations are equivalent:

SomeFunctionalInterface l = (String s, Integer i) -> s + " " + i.toString();
SomeFunctionalInterface l = (s, i) -> s + " " + i.toString();

Note that in the second case, the parameters' types are not specified. They are known to the compiler from the definition of the target type SomeFunctionalInterface.

Variable Capture

Lambda expressions are allowed to use the variables present in the scope in which the lambda is declared. For this reason, they are called capturing lambdas. However, different types of variable behave differently:

Class and instance variables can be used without any special concern. Capturing an instance variable can be seen as capturing the final local variable this. Class and instance variables are accessible from the lambda code, and they can be modified by the lambda - subject to safe concurrent access rules. Note that according to the functional programming philosophy, lambdas should not mutate external state, as a side-effect.

Local variables have to be explicitly declared final or be effectively final. Local variables are allocated on a thread's stack, and they are deallocated when the thread stack unwinds. If a lambda accessed a non-final local variable, and was executed from another thread - which is common for lambdas - it could get into the situation where it accesses a deallocated variable, which would cause a runtime error. To protect against this situation, the local variables captured by lambdas have to be explicitly declared final, or be effectively final, so the content of the variable is copied in the lambda block. This way, the lambda will always access a copy of a local variable, and not the original variable.

From this perspective, lambdas are not closures. Closures can refer variables - including variables local to the scope the closure was defined in - with no restriction. Lambdas close over values, not variables.

Method Reference

A lambda expression can be replaced with a method reference if its body consists of only one method invocation, in one of the cases described below:

  1. Lambda expression body consists of a class method invocation
  2. Lambda expression body consists of an instance method invocation on the first lambda argument
  3. Lambda expression body consists of an instance method invocation on an external expression
  4. Constructor method reference
  5. Array constructor method reference
  6. Super-call method reference

A method reference is a syntactically friendly way of referring to the code of a method by the method name, and use this name as a lambda expression. The idea is that if a lambda represents "call just this one method directly", then it is best to refer to the method by name, rather than a description of how to call it. Presumably, this improves code readability. When specifying a method reference, no brackets are needed because specifying the method reference is not actually calling the method, but passing code around.

The compiler goes through a similar type checking process for method references as for lambdas: the signature of the method reference has to match the target type inferred from the context.

Lambda Expression Body Consists of a Class Method Invocation

If the lambda's body consists in a single static method invocation, the lambda can be replaced with the static method's reference as follows:

(args) -> ClassName.staticMethod(args)

is equivalent with:

ClassName::staticMethod

Example:

(String s) -> Integer.parseInt(s)

is equivalent with:

Integer::parseInt

Lambda Expression Body Consists of an Instance Method Invocation on the First Lambda Argument

(arg0, rest) -> arg0.instanceMethod(rest)

is equivalent with:

arg0-ClassName::instanceMethod

where arg0 is of type ClassName and rest may contain zero or more arguments.

Single-argument instance method reference example:

(Apple a) -> a.getWeight()

is equivalent with:

Apple::getWeight

Two-argument instance method reference example:

(String s, int i) -> s.substring(i)

is equivalent with:

String::substring

Lambda Expression Body Consists of an Instance Method Invocation on an External Expression

This case refers to calling the method in the lambda onto an external object that already exists:

(args) -> expr.instanceMethod(args)

is equivalent with:

expr::instanceMethod

No-argument example:

() -> Thread.currentThread().dumpStack()

is equivalent with:

Thread.currentThread()::dumpStack

Single-argument example:

(String s) -> System.out.println(s)

is equivalent with:

System.out::println

Constructor Method Reference

ClassName::new

Array Constructor Method Reference

Super-Call Method Reference

Lambda Composition

Some of the functional interfaces provide methods that allow composition: combination of several simple lambda expressions to build more complicated ones.