Java Tutorial 0/145 lessons ~6 min read Lesson 42

    Lambda Expressions & Functional Interfaces

    java lambda expressions functional interface Predicate Function Consumer Supplier method reference effectively final

    Course progress0%
    Focus
    28 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    In the previous lesson, you learned about the Java Streams API, which introduced a declarative and functional way of processing collections.

    You may have noticed code like this:

    code
    names.stream()
    .filter(name -> name.startsWith("J"))
    .forEach(System.out::println);

    What does this mean?

    code
    name -> name.startsWith("J")

    This is called a Lambda Expression.

    Before Java 8, developers had to create anonymous classes for simple behaviors, resulting in verbose code.

    Java 8 introduced Lambda Expressions and Functional Interfaces to simplify programming and bring functional programming concepts into Java.

    Lambda expressions are widely used in:

    • Spring Boot
    • Streams API
    • Microservices
    • CompletableFuture
    • Event Handling
    • Collections Sorting
    • Executors
    • Kafka Consumers
    • REST APIs

    Today, almost every enterprise Java application uses lambda expressions.

    In this lesson, you'll learn:

    • What is a Lambda Expression?
    • Why Lambdas?
    • Lambda Syntax
    • Functional Interfaces
    • @FunctionalInterface
    • Built-in Functional Interfaces
    • Predicate
    • Function
    • Consumer
    • Supplier
    • UnaryOperator
    • BinaryOperator
    • Method References
    • Variable Capture
    • Effectively Final Variables
    • Real-world Examples
    • Best Practices
    • Common Mistakes
    • Hands-on Exercises
    • Professional Interview Questions

    What is a Lambda Expression?

    A Lambda Expression is a concise way to represent an anonymous function.

    It allows you to pass behavior as data.

    General Syntax:

    code
    (parameters) -> expression

    Or

    code
    (parameters) -> {
    // statements
    }

    Example:

    code
    (a, b) -> a + b

    Meaning:

    • Input → a, b
    • Output → a + b

    Why Lambda Expressions?

    Before Java 8:

    code
    Runnable runnable = new Runnable() {
    @Override
    public void run() {
    System.out.println("Running");
    }
    };

    Java 8:

    code
    Runnable runnable =
    () -> System.out.println("Running");

    Much cleaner and easier to read.

    Benefits:

    • Less Boilerplate
    • Better Readability
    • Functional Programming
    • Easy Parallel Processing
    • Better Stream Integration

    Lambda Syntax

    No Parameters

    code
    () -> System.out.println("Hello");

    One Parameter

    code
    name -> System.out.println(name);

    Parentheses are optional for a single parameter.

    Multiple Parameters

    code
    (a, b) -> a + b;

    Multiple Statements

    code
    (name) -> {
    System.out.println(name);
    return name.length();
    };

    Functional Interface

    A Functional Interface contains exactly one abstract method.

    Example:

    code
    @FunctionalInterface
    interface Calculator {
    int add(int a, int b);
    }

    Implementation using Lambda:

    code
    Calculator calculator =
    (a, b) -> a + b;
    System.out.println(calculator.add(10,20));

    Output:

    code
    30

    @FunctionalInterface Annotation

    code
    @FunctionalInterface
    interface Greeting {
    void sayHello();
    }

    Benefits:

    • Compiler validation
    • Prevents multiple abstract methods
    • Improves readability

    Built-in Functional Interfaces

    Java provides several commonly used functional interfaces.

    code
    Predicate
    Function
    Consumer
    Supplier
    UnaryOperator
    BinaryOperator

    These reside in the java.util.function package.

    Predicate<T>

    Represents a condition.

    Method:

    code
    boolean test(T value)

    Example:

    code
    Predicate<Integer> even =
    number -> number % 2 == 0;
    System.out.println(even.test(10));

    Output:

    code
    true

    Common Uses:

    • Filtering
    • Validation
    • Business Rules

    Function<T,R>

    Transforms one value into another.

    Method:

    code
    R apply(T input)

    Example:

    code
    Function<String,Integer> length =
    text -> text.length();
    System.out.println(length.apply("Java"));

    Output:

    code
    4

    Consumer<T>

    Consumes a value without returning anything.

    Method:

    code
    void accept(T value)

    Example:

    code
    Consumer<String> printer =
    System.out::println;
    printer.accept("Hello");

    Output:

    code
    Hello

    Supplier<T>

    Produces data without taking input.

    Method:

    code
    T get()

    Example:

    code
    Supplier<Double> random =
    Math::random;
    System.out.println(random.get());

    UnaryOperator<T>

    Accepts and returns the same type.

    code
    UnaryOperator<Integer> square =
    n -> n * n;
    System.out.println(square.apply(5));

    Output:

    code
    25

    BinaryOperator<T>

    Accepts two parameters of the same type and returns the same type.

    code
    BinaryOperator<Integer> sum =
    Integer::sum;
    System.out.println(sum.apply(20,30));

    Output:

    code
    50

    Method References

    Method references provide an even shorter syntax.

    Instead of:

    code
    names.forEach(name ->
    System.out.println(name));

    Use:

    code
    names.forEach(System.out::println);

    Types of Method References:

    code
    Class::staticMethod
    object::instanceMethod
    Class::instanceMethod
    Class::new

    Constructor Reference

    code
    Supplier<List<String>> list =
    ArrayList::new;

    Equivalent to:

    code
    () -> new ArrayList<>();

    Variable Capture

    A lambda can use variables from its surrounding scope.

    Example:

    code
    String prefix = "Hello";
    Consumer<String> consumer =
    name -> System.out.println(
    prefix + " " + name);

    Effectively Final Variables

    Captured variables cannot be modified after initialization.

    Valid:

    code
    int age = 20;
    Consumer<String> consumer =
    name -> System.out.println(age);

    Invalid:

    code
    age++;
    Consumer<String> consumer =
    name -> System.out.println(age);

    Compilation Error.

    Lambda with Streams

    code
    List<String> names =
    List.of("Java","Spring","Kafka");
    names.stream()
    .filter(name -> name.startsWith("J"))
    .map(String::toUpperCase)
    .forEach(System.out::println);

    Output:

    code
    JAVA

    Real-World Example: Employee Filtering

    code
    employees.stream()
    .filter(emp -> emp.getSalary() > 100000)
    .forEach(System.out::println);

    Real-World Example: Sorting Products

    code
    products.sort(
    (a,b) ->
    Double.compare(
    a.getPrice(),
    b.getPrice()
    ));

    Real-World Example: Email Notification

    code
    Consumer<String> emailService =
    email ->
    System.out.println(
    "Sending Email to " + email);
    emailService.accept(
    "user@example.com");

    Lambda vs Anonymous Class

    LambdaAnonymous Class
    ConciseVerbose
    Functional Interface OnlyAny Interface/Class
    No separate anonymous typeCreates anonymous class
    Better readabilityMore boilerplate

    Best Practices

    Use Method References

    Prefer:

    code
    System.out::println

    when it improves readability.

    Keep Lambdas Short

    Complex business logic should be moved to separate methods.

    Prefer Built-in Functional Interfaces

    Reuse Predicate, Function, Consumer, and Supplier instead of creating custom interfaces unnecessarily.

    Avoid Side Effects

    Lambda expressions should preferably be stateless and avoid modifying shared variables.

    Use Meaningful Parameter Names

    Improve readability by choosing descriptive names.

    Common Mistakes

    Using Lambdas with Non-Functional Interfaces

    Lambdas require exactly one abstract method.

    Writing Large Lambdas

    Large blocks reduce readability.

    Modifying Captured Variables

    Only effectively final variables can be captured.

    Ignoring Method References

    Method references often make code simpler.

    Creating Custom Interfaces Unnecessarily

    Many requirements are already covered by the built-in functional interfaces.

    Hands-on Exercise

    Create a Java program that:

    1. Creates a custom functional interface named Calculator.
    2. Implements it using lambda expressions.
    3. Uses Predicate to filter even numbers.
    4. Uses Function to convert names to uppercase.
    5. Uses Consumer to print employee details.
    6. Uses Supplier to generate random numbers.
    7. Uses UnaryOperator to calculate squares.
    8. Uses BinaryOperator to calculate sums.
    9. Uses method references with forEach().
    10. Processes employee objects using Streams and lambda expressions.

    Summary

    Lambda expressions bring functional programming capabilities to Java by allowing behavior to be passed as data. Combined with functional interfaces, method references, and the Streams API, lambdas enable concise, expressive, and maintainable code. They are foundational to modern Java development and are extensively used in Spring Boot, reactive programming, and enterprise applications.

    Key Takeaways

    • Lambda expressions represent anonymous functions.
    • Functional interfaces contain exactly one abstract method.
    • @FunctionalInterface enables compiler validation.
    • Predicate performs boolean tests.
    • Function transforms data.
    • Consumer consumes data without returning a value.
    • Supplier provides data without input.
    • UnaryOperator and BinaryOperator specialize common transformations.
    • Method references improve readability.
    • Captured variables must be effectively final.
    • Lambdas integrate seamlessly with the Streams API.

    Professional Interview Questions

    1What is a Lambda Expression in Java?

    Professional Answer

    A lambda expression is a concise representation of an anonymous function that can be passed as an implementation of a functional interface. It enables functional programming in Java, reduces boilerplate code, and improves readability, particularly when working with the Streams API, event handling, and asynchronous programming.

    Follow-up Questions

    • Which Java version introduced lambda expressions?
    • Can lambdas be used with any interface?

    Interview Tip: Remember: Lambda = Anonymous Function + Functional Interface.

    2What is a Functional Interface?

    Professional Answer

    A functional interface is an interface that contains exactly one abstract method. It may also include default, static, or private methods. Functional interfaces serve as the target types for lambda expressions and method references. The @FunctionalInterface annotation helps the compiler enforce this rule.

    Follow-up Questions

    • Can a functional interface contain default methods?
    • Name some built-in functional interfaces.

    Interview Tip: Common built-in functional interfaces include: Predicate, Function, Consumer, Supplier.

    3What is the difference between Predicate, Function, Consumer, and Supplier?

    Professional Answer

    Predicate evaluates a condition and returns a boolean value. Function transforms an input into an output. Consumer accepts an input and performs an action without returning a result. Supplier produces and returns a value without accepting any input. These interfaces form the foundation of functional programming in Java and are widely used with the Streams API.

    Follow-up Questions

    • Which interface is commonly used with filter()?
    • Which interface powers map()?
    • Which interface is useful for lazy object creation?

    Interview Tip: A quick way to remember: Predicate → Test, Function → Transform, Consumer → Consume, Supplier → Supply.

    Ready to mark this lesson complete?Track your journey across the entire course.