Java tutorials > Modern Java Features > Java 8 and Later > What are lambda expressions and how do they work?

What are lambda expressions and how do they work?

Lambda expressions, a cornerstone of Java 8 and later, provide a concise way to represent anonymous functions. They enable you to treat functionality as a method argument, or code as data. This tutorial explores lambda expressions, covering their syntax, functionality, and usage with code examples.

Introduction to Lambda Expressions

Lambda expressions are essentially anonymous methods – methods without a name. They are characterized by:

  • No name: Lambda functions don't have a name like regular methods.
  • Parameter list: Like methods, they can have zero or more parameters.
  • Body: Contains the logic of the function.
  • Return type: Inferred by the compiler or explicitly specified.

The general syntax is (parameters) -> { body }.

Basic Syntax of Lambda Expressions

Let's break down the syntax:

  • () -> System.out.println("Hello, Lambda!");: A lambda expression with no parameters that prints "Hello, Lambda!".
  • (int x, int y) -> x + y;: A lambda expression that takes two integer parameters (x and y) and returns their sum. The return type is inferred as int.
  • x -> x * x;: A lambda expression taking a single parameter x and returning its square. Parentheses around a single parameter are optional.

() -> System.out.println("Hello, Lambda!");
(int x, int y) -> x + y;
x -> x * x;

Functional Interfaces

Lambda expressions are primarily used with functional interfaces. A functional interface is an interface with only one abstract method. Java provides several built-in functional interfaces in the java.util.function package, such as:

  • Function: Represents a function that accepts one argument and produces a result.
  • Consumer: Represents an operation that accepts a single input argument and returns no result.
  • Predicate: Represents a predicate (boolean-valued function) of one argument.
  • Supplier: Represents a supplier of results.

Lambda Expressions with Functional Interfaces - Example

In this example:

  • We import the Function interface from java.util.function.
  • We define a lambda expression x -> x * x and assign it to a Function variable named square. This lambda takes an integer and returns its square.
  • We use the apply() method of the Function interface to execute the lambda expression with an input of 5.
  • The result is then printed to the console.

The second part shows how to use the Consumer interface.

import java.util.function.Function;

public class LambdaExample {
    public static void main(String[] args) {
        // Using Function interface to square a number
        Function<Integer, Integer> square = x -> x * x;
        int result = square.apply(5); // Applies the function
        System.out.println("Square of 5: " + result); // Output: Square of 5: 25

        //Using Consumer to print a message.
        java.util.function.Consumer<String> printMessage = message -> System.out.println(message);
        printMessage.accept("This is a Consumer example."); // Output: This is a Consumer example.
    }
}

Concepts Behind the Snippet

The core concept is functional programming. Lambda expressions enable treating functions as first-class citizens, meaning functions can be passed as arguments, returned from other functions, and assigned to variables. This allows for more concise and flexible code, particularly when working with collections and streams.

Real-Life Use Case Section

A common use case is processing collections using streams. The example above shows how to use lambda expressions with streams to:

  • Filter even numbers from a list.
  • Square each even number.
  • Collect the squared even numbers into a new list.

Lambda expressions make stream operations much more readable and concise.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamLambdaExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // Using lambda to filter even numbers and then square them
        List<Integer> evenSquares = numbers.stream()
                .filter(x -> x % 2 == 0) // Filter even numbers
                .map(x -> x * x)       // Square the even numbers
                .collect(Collectors.toList()); // Collect the results into a list

        System.out.println("Even Squares: " + evenSquares); // Output: Even Squares: [4, 16, 36, 64, 100]
    }
}

Best Practices

  • Keep lambdas short and focused: Complex logic should be extracted into separate methods.
  • Use descriptive variable names: This enhances readability.
  • Consider method references: If a lambda expression simply calls an existing method, use a method reference (e.g., System.out::println) for even more concise code.
  • Avoid side effects: Lambdas should ideally be pure functions – they should not modify external state.

Interview Tip

Be prepared to explain the difference between lambda expressions and anonymous classes. Lambda expressions can only be used with functional interfaces, whereas anonymous classes can implement interfaces with multiple methods or extend concrete classes. Lambda expressions are also generally more lightweight and efficient.

When to use them

Use lambda expressions when:

  • You need to pass a small piece of code as an argument to a method.
  • You are working with functional interfaces (e.g., in streams or event handlers).
  • You want to write concise and readable code for simple operations.

Memory footprint

Lambda expressions generally have a smaller memory footprint than anonymous classes, especially when used frequently. The exact difference depends on the JVM implementation and the complexity of the lambda, but lambdas are often compiled to more efficient bytecode.

Alternatives

Alternatives to lambda expressions include:

  • Anonymous classes: As mentioned earlier, anonymous classes can be used instead of lambda expressions, but they are more verbose.
  • Regular methods: You can define a regular method and pass a method reference to it, but this is less flexible for simple operations.

Pros

  • Conciseness: Lambda expressions significantly reduce boilerplate code.
  • Readability: They often make code easier to understand, especially when used with streams.
  • Flexibility: They allow for treating functions as first-class citizens.

Cons

  • Debugging can be challenging: Debugging lambda expressions can sometimes be more difficult than debugging regular methods.
  • Readability can suffer with complex lambdas: Overly complex lambda expressions can make code harder to read.

FAQ

  • Can I use lambda expressions with any interface?

    No, you can only use lambda expressions with functional interfaces – interfaces with a single abstract method.
  • What is a method reference?

    A method reference is a shorthand notation for a lambda expression that simply calls an existing method. For example, System.out::println is a method reference that calls the println method of the System.out object.
  • Are lambda expressions more efficient than anonymous classes?

    Generally, yes. Lambda expressions are often compiled to more efficient bytecode and have a smaller memory footprint than anonymous classes.