Tutorials

The Ultimate Guide to Lambda Expressions in Java

Lambda expressions let you define "anonymous methods" that are concise and can be passed as arguments. They align well with functional programming paradigms.

This guide walks you through lambda expressions in Java, covering their purpose, syntax, advantages, and use cases.

Key Takeaways

  • Lambda expressions provide a concise syntax to implement functional interfaces.
  • They streamline working with collections and enable method references.
  • Use lambda expressions for cleaner, more expressive code.

What are Lambdas in Java?

Lambdas allow you to implement functional interfaces more concisely, as shown here:

interface Print {
    void print(String message);
}

public class MainClass {
    public static void main(String[] args) {
        Print myPrinter = x -> System.out.println(x);
        myPrinter.print("Hello lambdas!");
    }
}

In this example, Print is a functional interface with one abstract method. The lambda expression x -> System.out.println(x) implements this method succinctly.

Passable as Function Arguments

Lambdas can be passed as arguments to functions. Here's an example with a math operation:

interface MathOp {
    int operate(int a, int b);
}

public class MainClass {
    public static void printResults(int x) {
        System.out.println(x);
    }

    public static void main(String[] args) {
        MathOp add = (a, b) -> a + b;
        printResults(add.operate(2, 2)); //prints 4
    }
}

The lambda add implements the MathOp interface, showing how lambdas can simplify code.

Lambda Syntax

A lambda in Java consists of:

1) Argument List

This is a comma-separated list of parameters, like (a, b). Types are inferred.

2) Arrow Token

The arrow token -> separates parameters from the body.

3) Body

The body is the implementation. For single expressions, you can omit curly braces and return.

Why Use Lambdas in Java?

Concise Syntax

Lambdas reduce boilerplate compared to anonymous classes. Here's a comparison:

interface Print {
    void print(String message);
}

public class MainClass {
    public static void main(String[] args) {
        Print myNestedPrinter = new Print() {
            public void print(String x) {
                System.out.println(x);
            }
        };

        Print myPrinter = x -> System.out.println(x);

        myNestedPrinter.print("Hello nested classes");
        myPrinter.print("Hello lambdas!");
    }
}

You can see that using lambdas shorten the code and improve readability.

Ease with Collections

Using lambdas with collections is elegant, notably with the Streams API:

import java.util.*;

public class MainClass {
    public static void main(String[] args) {
        List list = Arrays.asList('a', 'b', 'c');
        list.forEach(x -> System.out.println(x));
    }
}

The example above uses forEach with a lambda for concise iteration.

Method References

Use method references when a lambda directly calls an existing method.

interface Print {
    void print(String msg);
}

public class MainClass {
    public static void systemPrint(String x) {
        System.out.println(x);
    }

    public static void main(String[] args) {
        Print methodPrint = MainClass::systemPrint;
        methodPrint.print("Method reference");
    }
}

Here, MainClass::systemPrint is a method reference, replacing a lambda expression.

The java.util.function Package

The java.util.function package offers ready-to-use functional interfaces.

import java.util.function.Consumer;

public class MainClass {
    public static void main(String[] args) {
        Consumer myConsumer = x -> System.out.println(x);
        myConsumer.accept("Hello world!");
    }
}

The Consumer is a generic interface, perfect for actions on single arguments.

Lambda Scope and Lexical Scoping

Lambdas are lexically scoped, meaning they capture variables as they are in their surrounding context, provided the variables are effectively final:

interface Print {
    void print();
}

public class MainClass {
    public static void main(String[] args) {
        String message = "Hello world!";
        Print myPrinter = () -> {
            System.out.println(message);
        };
        myPrinter.print();
    }
}

Here, message is final, accessible from within the lambda.

Conclusion

Java's support for lambda expressions enhances a functional programming approach, making code concise and expressive. Dive deeper into lambdas through resources like Dhruv Rai Puri's explanation of internal vs. external iterators or Esteban Herrera's article on method references.

FAQ

What is a functional interface?

A functional interface is an interface with only one abstract method, suitable for lambda expressions.

Can lambdas have multiple statements?

Yes, for multiple statements use curly braces and explicit returns in the lambda body.

What is the main benefit of using lambdas?

Lambdas make code more readable and concise, especially for implementing single-method interfaces.

How are lambdas different from anonymous classes?

Unlike anonymous classes, lambdas are lighter and don't retain a lexical scope, making them easier to use for functional programming.

Mastering the tech interviewWhat everyone is doing wrong in tech interviews