What are lambda expressions in Java and how do they work?

Java code
Published on May 31, 2026
#Java#InterviewQuestions#LambdaExpressions
Quick Answer: A lambda expression is a concise anonymous function that implements a functional interface, written as (params) -> body, enabling you to pass behaviour as a value in Java 8+.

The Interview Question

"What are lambda expressions in Java and how do they work?"

Short Answer

A lambda expression is an anonymous, inline implementation of a functional interface — an interface with exactly one abstract method. Introduced in Java 8, lambdas let you treat behaviour as a first-class value: pass functions to methods, store them in variables, and return them from methods.

Syntax: (parameters) -> expression or (parameters) -> { statements; }

Under the hood the compiler uses the invokedynamic bytecode instruction and LambdaMetafactory to link the lambda at runtime — more efficient than generating an anonymous inner class for every lambda.

Detailed Explanation

1. Functional Interfaces — The Foundation

A lambda can only be used where a functional interface is expected. A functional interface has exactly one abstract method (but may have many default/static methods). The @FunctionalInterface annotation makes the contract explicit and causes a compile error if you accidentally add a second abstract method.

@FunctionalInterface
interface Greeter {
    String greet(String name);  // single abstract method
}

2. Lambda Syntax Variants

Java allows several forms to keep lambdas concise:

  • () -> "hello" — no parameters, expression body (implicit return)
  • x -> x * 2 — one parameter (parentheses optional), expression body
  • (x, y) -> x + y — two parameters, expression body
  • (int x, int y) -> { int sum = x+y; return sum; } — explicit types, block body

3. Capturing Variables — Effectively Final

A lambda can reference variables from its enclosing scope. Local variables must be effectively final — never reassigned after their first assignment. Instance and static variables have no such restriction because they live on the heap and the lambda holds a reference to this.

Why effectively final? Lambdas may run in a different thread or at a later time. Java captures a copy of local variables into the lambda. If the variable were mutable, the copy could go stale, leading to data races. The JVM avoids the problem by requiring immutability.

4. Lambda vs. Anonymous Inner Class

Before Java 8, you implemented functional interfaces with anonymous inner classes. Lambdas replace this boilerplate:

// Before Java 8 — anonymous inner class
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("running");
    }
};

// Java 8+ — lambda
Runnable r2 = () -> System.out.println("running");

Key differences from anonymous classes:

  • No new scope: this inside a lambda refers to the enclosing class, not the lambda.
  • No separate .class file: lambdas use invokedynamic — no extra class loading overhead.
  • More concise: dramatically less boilerplate.

5. Common Built-in Uses

The java.util.function package ships ready-made functional interfaces for the most common patterns:

  • Comparator<T> — compare two objects for sorting
  • Runnable — run a task with no parameters/return
  • Function<T,R> — transform T into R
  • Predicate<T> — test a condition on T
  • Consumer<T> — process T, return nothing
  • Supplier<T> — produce a T with no input

Code Example

import java.util.Arrays;
import java.util.List;
import java.util.function.*;

public class LambdaDemo {

    private String prefix = "Hello, ";   // instance variable — freely accessible

    public void run() {

        // ── 1. Comparator — sort a list ───────────────────────────────────
        List<String> names = Arrays.asList("Charlie", "Alice", "Dave", "Bob");
        names.sort((a, b) -> a.compareTo(b));      // lambda replaces anonymous class
        System.out.println("Sorted: " + names);    // [Alice, Bob, Charlie, Dave]

        // ── 2. Runnable — no params, no return ────────────────────────────
        Runnable task = () -> System.out.println("Task running in thread: "
                + Thread.currentThread().getName());
        new Thread(task).start();

        // ── 3. forEach with Consumer ──────────────────────────────────────
        names.forEach(name -> System.out.println(prefix + name));
        //  "this.prefix" works — lambdas see instance variables freely

        // ── 4. Effectively final capture ──────────────────────────────────
        String greeting = "Hi";    // effectively final — never reassigned
        Greeter g = name -> greeting + ", " + name + "!";
        System.out.println(g.greet("Alice"));   // Hi, Alice!

        // greeting = "Hey";  // would make 'greeting' NOT effectively final
        //                    // → compile error in the lambda above

        // ── 5. Function chain ─────────────────────────────────────────────
        Function<String, String> trim    = String::trim;
        Function<String, String> upper   = String::toUpperCase;
        Function<String, String> process = trim.andThen(upper);
        System.out.println(process.apply("  techoral  "));  // TECHORAL

        // ── 6. Predicate composition ──────────────────────────────────────
        Predicate<Integer> isPositive = n -> n > 0;
        Predicate<Integer> isEven     = n -> n % 2 == 0;
        Predicate<Integer> isPositiveEven = isPositive.and(isEven);

        List.of(-2, 1, 4, 7, 8).stream()
            .filter(isPositiveEven)
            .forEach(System.out::println);  // 4, 8

        // ── 7. Supplier — deferred/lazy value ─────────────────────────────
        Supplier<List<String>> listFactory = () -> new java.util.ArrayList<>();
        List<String> fresh = listFactory.get();
        fresh.add("item");
        System.out.println(fresh);  // [item]

        // ── 8. Block body with explicit return ────────────────────────────
        Function<Integer, String> classify = n -> {
            if (n < 0)  return "negative";
            if (n == 0) return "zero";
            return "positive";
        };
        System.out.println(classify.apply(-5));   // negative
        System.out.println(classify.apply(0));    // zero
        System.out.println(classify.apply(42));   // positive
    }

    @FunctionalInterface
    interface Greeter {
        String greet(String name);
    }

    public static void main(String[] args) {
        new LambdaDemo().run();
    }
}

When to Use

Use lambdas when:

  • Passing a short piece of behaviour to a method (sorting comparators, event handlers, stream operations).
  • Replacing single-method anonymous inner classes to reduce boilerplate.
  • Building flexible APIs that accept strategies or callbacks as parameters.
  • Writing functional-style pipelines with the Streams API.

Consider alternatives when:

  • The lambda body is long and complex — extract it to a named method for readability.
  • A method reference (e.g., String::toUpperCase) is even more concise.
  • The logic needs to be reused in many places — a named method or class is more maintainable.

Why This Question Matters in Interviews

Lambda expressions are a cornerstone of modern Java. Interviewers ask this to verify:

  • You understand functional interfaces and can identify them.
  • You know the effectively final rule and can explain why it exists.
  • You understand the difference between expression bodies and block bodies.
  • You can explain how lambdas differ from anonymous inner classes (scope of this, no extra .class file).
  • You have practical experience using lambdas with Comparator, Runnable, Stream operations, and java.util.function types.

Common Pitfalls

  • Trying to mutate a captured local variable — causes a compile error. Use an array wrapper, AtomicInteger, or instance variable if mutation is needed.
  • Confusing this inside a lambda vs. anonymous class — in a lambda, this is the enclosing class; in an anonymous class, this is the anonymous class instance.
  • Using lambdas for complex logic — a 20-line lambda is harder to read than a named method. Keep lambdas short and extract long logic.
  • Checked exceptions in lambdas — standard functional interfaces don't declare checked exceptions. You must either wrap in a try/catch inside the lambda or use a custom functional interface that throws the checked exception.
  • Serialization — lambdas are not easily serializable. If you need to serialize behaviour, use a concrete class.

Conclusion

Lambda expressions transform Java from a purely object-oriented language into a hybrid functional language. They eliminate boilerplate anonymous inner classes, enable clean stream pipelines, and allow behaviour to be passed as data. Mastering the syntax, understanding the effectively-final capture rule, and knowing when a lambda is the right tool versus a method reference or named class will make you a more effective Java developer and help you answer this common interview question with confidence.