Lambda expressions are a new feature in Java 8 that provide a concise way to represent a function. Unlike normal methods in Java, lambda expressions do not have a name and can be defined directly in the body of an existing method.

Syntax of lambda expressions

A lambda expression is defined using two parts — a list of parameters, and an expression or code block that returns a value. For example, a lambda expression that takes in two integers and returns their sum can be defined like so:

(int a, int b) -> a + b

The parameter and the return types of a lambda expression can be inferred automatically by the Java compiler (how this is done is discussed in the next section). As a result, we generally omit the types when writing a lambda expression. The above lambda expression can be rewritten like this:

(a, b) -> a + b

If there is a single parameter and the parameter type is omitted, the parenthesis around the parameter list can be removed. For example, a Lambda expression to double a value can be written like this:

a -> 2 * a

If you need to include if statements, for loops, or other kinds of statements, then you need to enclose your code in a code block and return a value like this:

(a, b) -> {
    System.out.println("parameters = " + a + ", " + b);
    return a + b;
}

Defining a lambda expression in Java

As mentioned previously, the parameter and return types of the lambda expression can be automatically inferred by the Java compiler. For this to work, we should first define a "functional interface", which is an interface with a single method only.

For example, to define lambda expressions that take two ints as parameters and returns an int, we can define an interface like this:

interface TwoIntegerOperation {
    int run(int a, int b);
}

The method in the interface doesn't have to be named run — you can use any other name of your choice, as long as you use the same name elsewhere.

Next, we can define lambda expressions like this, and run it by calling the run method of the lambda expression.

TwoIntegerOperation add = (a, b) -> a + b;
System.out.println("3 + 2 = " + add.run(3, 2));
// prints 3 + 2 = 5

TwoIntegerOperation subtract = (a, b) -> a - b;
System.out.println("4 - 3 = " + subtract.run(4, 3));
// prints 4 - 3 = 1

Even though the parameter and return types aren't defined in the lambda expression itself, it is automatically inferred from the fact that add and subtract variables are instances of TwoIntegerOperation.

A common use case for lambda expressions is to pass them to a method. Continuing with our TwoIntegerOperation example, we can define a method that takes in two integers and a lambda expression, and then runs it:

public static int executeOperation(int a, int b, TwoIntegerOperation op) {
    int result = op.run(a, b);
    return result;
}

public static void main(String[] args) {
    TwoIntegerOperation add = (a, b) -> a + b;
    System.out.println("3 + 2 = " + executeOperation(3, 2, add));
    // prints 3 + 2 = 5

    TwoIntegerOperation subtract = (a, b) -> a - b;
    System.out.println("4 - 3 = " + executeOperation(4, 3, subtract));
    // prints 4 - 3 = 1
}

We can also write the lambda expression inline without declaring a separate variable, as shown below.

System.out.println("3 + 2 = ", executeOperation(3, 2, (a, b) -> a + b));
// prints 3 + 2 = 5

System.out.println("4 - 3 = ", executeOperation(4, 3, (a, b) -> a - b));
// prints 4 - 3 = 1

In this case, we didn't define a separate variable. However, since the third argument of executeOperation is a TwoIntegerOperation, so the Java compiler infers that the lambda expression must have the same parameter and return types as defined in the interface.

Using lambda expressions with built-in Java libraries

In the above example, we went through an example of defining our own functional interface. However, for many common operations, Java offers a number of predefined functional interfaces in the java.util.function package.

For example, the built-in Predicate<T> interface takes in a generic type T (which could be an Integer, String or any other data type based on the context) and returns a boolean. This can be used to perform operations on a List, such as removing elements based on a condition. An example is shown below:

List<Integer> list = new ArrayList<>();
list.add(11);
list.add(50);
list.add(22);
list.add(30);
list.add(40);

Predicate<Integer> isElementLessThan30 = element -> element < 30;
list.removeIf(isElementLessThan30);
System.out.println(list);
// prints [50, 30, 40]

Here, the removeIf() method calls the lambda expression on every element of the list to determine if it should be removed. Since the isElementLessThan30 lambda expression evaluates to false for 11 and 22, they are removed from the list.

We can simplify our code by defining the lambda expression inline without a separate variable:

list.removeIf(element -> element < 30);
System.out.println(list);

Similarly, the BinaryOperator<T> interface takes in two elements of the generic type T, and returns a result of the same generic type T. This can be used to perform various operations on the array by making use of Java Streams, such as summing up all the elements of an array. An example is shown below:

List<Integer> list = new ArrayList<>();
list.add(11);
list.add(50);
list.add(22);
list.add(30);
list.add(40);

BinaryOperator<Integer> sum = (subtotal, element) -> {
    System.out.println("adding " + subtotal + " and " + element);
    return subtotal + element;
};
int result = list.stream().reduce(0, sum);
System.out.println("total = " + result);

The program produces the following output:

adding 0 and 11
adding 11 and 50
adding 61 and 22
adding 83 and 30
adding 113 and 40
total = 153

Here, the reduce() method starts with the initial value of zero. It then calls the lambda expression on the initial value and the first element of the array, yielding a subtotal of 11. Next, it calls the lambda expression with the subtotal of 11 and the second element, yielding a new subtotal of 61. This process is continued for all the elements, and we arrive at the total of 153.