Back to Lambda Expressions
Q1Question 1 of 5

What are lambda expressions?

Key Concepts
  • A lambda expression is a concise anonymous function with syntax: (parameters) -> expression or (parameters) -> { statements; }.
  • Lambdas replace verbose anonymous inner classes when implementing functional interfaces (single abstract method).
  • The compiler infers parameter types from the target context, so you can usually omit them: t -> t.getAmount().
  • Lambdas capture local variables from the enclosing scope, but those variables must be effectively final.
  • Under the hood, lambdas use invokedynamic bytecode instead of generating anonymous class files, making them faster to instantiate.
  • The this reference inside a lambda refers to the enclosing class, not the lambda itself, unlike anonymous classes.
  • Method references like Trade::getAmount are shorthand for lambdas that call a single existing method.

Definition

A lambda expression is a concise anonymous function with syntax (params) -> body that implements a functional interface's single abstract method, replacing verbose anonymous inner classes.
DIAGRAM
- **Lambda syntax** is `(parameters) -> expression` for single expressions or `(parameters) -> { statements; }` for multi-line blocks.
- **Type inference** means the compiler determines parameter types from context, so `(Trade t) -> t.getAmount()` simplifies to `t -> t.getAmount()`.
- **Effectively final** capture means lambdas can reference local variables from the enclosing scope, but those variables must never be reassigned after initialisation.
- **invokedynamic** bytecode is used instead of generating anonymous `.class` files, making lambda instantiation faster and more memory-efficient.
- **this** inside a lambda refers to the enclosing class instance, not the lambda itself.
- **Method references** like `Trade::getAmount` are shorthand for lambdas that simply call one existing method.

Before Java 8, to tell National Bank of India's sorting system to sort accounts by balance descending, Rajesh had to write a full Comparator class with five lines of boilerplate. With lambdas, it becomes accounts.sort((a, b) -> b.getBalance().compareTo(a.getBalance())) in one line. It is like writing a quick instruction note instead of a formal three-page memo for the same task.

Understanding the Concepts

What is a lambda expression?

A lambda expression is a concise way to represent an anonymous function. It can be passed as an argument, stored in a variable, or returned from a method. It implements the single abstract method of a functional interface without the verbose boilerplate of an anonymous inner class.

What is the lambda syntax?

Three forms exist depending on the complexity of the body.

JAVA
package com.bank.core;

import java.math.BigDecimal;
import java.util.function.*;

public class LambdaSyntaxDemo {
    public static void main(String[] args) {
        // Form 1: explicit parameter type, single expression
        Function<BigDecimal, String> format1 =
            (BigDecimal amt) -> "Rs." + amt.toPlainString();

        // Form 2: inferred type, single expression (most common)
        Function<BigDecimal, String> format2 =
            amt -> "Rs." + amt.toPlainString();

        // Form 3: multi-statement block with explicit return
        Function<BigDecimal, String> format3 = amt -> {
            String prefix = amt.signum() < 0 ? "(Rs." : "Rs.";
            String suffix = amt.signum() < 0 ? ")" : "";
            return prefix + amt.abs().toPlainString() + suffix;
        };

        BigDecimal balance = new BigDecimal("-4500.00");
        System.out.println(format1.apply(balance));
        System.out.println(format3.apply(balance));
    }
}
TEXT
Rs.-4500.00
(Rs.4500.00)

How is a lambda different from an anonymous class?

| Aspect | Anonymous Class | Lambda |
|--------|----------------|--------|
| Syntax | 5+ lines with class body | 1 line with arrow |
| Bytecode | Generates separate .class file | Uses invokedynamic (no class file) |
| this reference | Refers to the anonymous class | Refers to the enclosing class |
| Multiple methods | Can implement multiple methods | Implements exactly one (SAM) |
| Instantiation | Creates new class instance | Lightweight method handle |

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.List;
import java.util.Comparator;

public class AnonVsLambda {
    public static void main(String[] args) {
        List<BigDecimal> balances = new java.util.ArrayList<>(List.of(
            new BigDecimal("50000"), new BigDecimal("125000"), new BigDecimal("8000")
        ));

        // BEFORE: anonymous class (verbose)
        balances.sort(new Comparator<BigDecimal>() {
            @Override
            public int compare(BigDecimal a, BigDecimal b) {
                return b.compareTo(a); // descending
            }
        });

        // AFTER: lambda (concise)
        balances.sort((a, b) -> b.compareTo(a));

        // EVEN SHORTER: method reference + Comparator factory
        balances.sort(Comparator.reverseOrder());

        System.out.println(balances);
    }
}
TEXT
[125000, 50000, 8000]

What does effectively final mean for captured variables?

A lambda can reference local variables from the enclosing scope, but those variables must be effectively final: either declared final or never reassigned after initialisation. This is because the lambda captures a copy of the value, not a reference to the variable binding.

JAVA
package com.bank.core;

import java.math.BigDecimal;
import java.util.List;
import java.util.function.Predicate;
import java.util.concurrent.atomic.AtomicInteger;

public class EffectivelyFinalDemo {
    public static void main(String[] args) {
        BigDecimal threshold = new BigDecimal("100000"); // effectively final
        Predicate<BigDecimal> isLarge = amt -> amt.compareTo(threshold) > 0;

        // threshold = BigDecimal.ZERO; // COMPILE ERROR! Not effectively final

        // Workaround for mutable counter:
        AtomicInteger counter = new AtomicInteger(0); // reference is final
        List.of(new BigDecimal("50000"), new BigDecimal("200000"))
            .stream()
            .filter(isLarge)
            .forEach(amt -> counter.incrementAndGet()); // OK

        System.out.println("Large accounts: " + counter.get());
    }
}
TEXT
Large accounts: 1

What is invokedynamic and why does it matter?

Lambdas do not create anonymous classes at compile time. They use the invokedynamic bytecode instruction which defers the implementation strategy to runtime. The JVM creates a lightweight implementation, often a method handle, without the overhead of loading a full class. This makes lambdas faster than anonymous classes for instantiation and uses less memory.

What are common mistakes with lambdas?

JAVA
package com.bank.core;

import java.math.BigDecimal;
import java.util.List;

public class LambdaMistakes {
    private String bankName = "National Bank";

    public void demo() {
        // WRONG: assuming 'this' refers to the lambda
        Runnable r = () -> {
            // 'this' refers to LambdaMistakes instance, NOT the lambda
            System.out.println(this.bankName); // prints "National Bank"
        };
        r.run();

        // WRONG: side effects in stream lambda
        List<BigDecimal> results = new java.util.ArrayList<>();
        List.of(new BigDecimal("1000"), new BigDecimal("2000")).stream()
            .forEach(results::add); // side effect! Unsafe in parallel

        // CORRECT: use collect
        List<BigDecimal> safe = List.of(new BigDecimal("1000"), new BigDecimal("2000")).stream()
            .toList();
    }
}

What about lambdas and Comparator composition?

Lambdas enable fluent Comparator composition that was impossible with anonymous classes.

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.Comparator;
import java.util.List;

public class ComparatorComposition {
    record Account(String id, String branch, BigDecimal balance) {}

    public static void main(String[] args) {
        List<Account> accounts = List.of(
            new Account("SAV2001", "Mumbai", new BigDecimal("50000")),
            new Account("SAV2002", "Mumbai", new BigDecimal("125000")),
            new Account("CUR3001", "Delhi", new BigDecimal("50000"))
        );

        // Multi-key sort: branch ascending, then balance descending
        accounts.stream()
            .sorted(Comparator.comparing(Account::branch)
                .thenComparing(Account::balance, Comparator.reverseOrder()))
            .forEach(a -> System.out.println(a.id() + " " + a.branch() + " Rs." + a.balance()));
    }
}
TEXT
CUR3001 Delhi Rs.50000
SAV2002 Mumbai Rs.125000
SAV2001 Mumbai Rs.50000

What should a senior engineer know?

Senior engineers should understand that lambdas are not always more readable than named methods. Complex multi-line lambdas should be extracted into named private methods and referenced with method references. They should know that lambdas serialize only if the target functional interface extends Serializable, which is rare and generally discouraged. They should recognise that lambda performance is nearly identical to anonymous classes after JIT compilation, but instantiation is faster due to invokedynamic.

Visual Overview

DIAGRAM
  Anonymous Class                        Lambda Expression
  ──────────────────                     ─────────────────
  new Comparator<Account>() {            (a, b) -> b.getBalance()
      @Override                                       .compareTo(
      public int compare(                              a.getBalance())
          Account a, Account b) {
          return b.getBalance()
              .compareTo(a.getBalance());
      }
  }

  5 lines → 1 line
  Generates .class file → Uses invokedynamic
  this = anonymous class → this = enclosing class

Key Takeaway

A lambda expression is a concise anonymous function that implements a functional interface's single abstract method. The syntax is parameters, arrow, and body. The compiler infers parameter types from the target context, so you can usually omit them. Lambdas replace the verbose anonymous inner class pattern that required a class declaration, method override, and curly braces for even the simplest operation.

In Rajesh's account sorting service at National Bank of India, the anonymous class approach required 20 lines. The lambda approach does the same in four lines: a stream pipeline with sorted using Comparator.comparing, filter with a lambda predicate, and map with a method reference. Lambdas are not only shorter but composable: Comparator.comparing(Account::branch).thenComparing(Account::balance, reverseOrder()) creates a multi-key sort in one expression.

Lambdas capture local variables from the enclosing scope, but those variables must be effectively final. The workaround for mutable capture is AtomicInteger or a final array wrapper. Under the hood, lambdas use invokedynamic bytecode instead of generating anonymous class files, making them faster to instantiate and more memory-efficient.

Remember one sentence: a lambda is (params) -> body, it implements one method of a functional interface, and captured variables must be effectively final.

---

Share
Use ← → arrow keys to navigate