Lambda expressions

Java 8 changed how Java code looks. Instead of writing a whole class just to pass one small piece of behaviour (“compare by price”, “keep only items under ₹500”), you write a lambda expression: a short, anonymous function. You'll learn the syntax, what a lambda can and can't use from around it, how the JVM actually runs one, and the list methods that make lambdas useful from day one.

Module 14 · Modern Java⏱ 22 minIntermediate9 practice problems
1 The story

Six lines to say “sort by price”

Riya wants the canteen menu sorted by price, then by name, then by popularity, each on a different screen. Every time she writes a new class implementing Comparator, with a method, types, braces… for one line of real logic.

menu.sort(new Comparator<Dish>() { public int compare(Dish a, Dish b) { return a.price - b.price; } }); anonymous class: 5 lines menu.sort((a, b) -> a.price - b.price); lambda: 1 line, same result myphatshala.in
Riya

Sir, the only line that matters is a.price - b.price. Why all this wrapping?

Arjun Sir

Before Java 8 there was no other way to pass behaviour. Now you can pass just the logic: (a, b) -> a.price - b.price. That's a lambda expression.

Riya

But sort expects a Comparator object. How does a line of code become an object?

Arjun Sir

The JVM builds the object for you. Let's shrink the class step by step, then look behind the screen.

2 Definition

What is a lambda expression?

Formal definition (write this in exams)

A lambda expression is an anonymous function, written as a parameter list, the arrow token -> and a body (a single expression or a block), that provides the implementation of the single abstract method of a functional interface. Its parameter and return types are inferred from the target functional interface type. A lambda may use local variables of the enclosing scope only if they are final or effectively final; it does not introduce a new scope for this. At run time a lambda evaluates to an object that implements the functional interface; the JVM creates it through the invokedynamic instruction.

In simple words

A lambda is a small piece of code you can hand to a method: “here's how to compare”, “here's what to keep”. Java wraps it into an object automatically.

Words you'll meet in this lesson

Arrow ->Separates parameters from the body.
Functional interfaceAn interface with exactly one abstract method (Lesson 14.2).
Target typeThe interface the lambda is assigned or passed to.
Type inferenceThe compiler works out the parameter types for you.
CaptureUsing a local variable from outside the lambda.
Effectively finalNever reassigned after being given its value.

Anonymous class vs lambda

Anonymous classLambda
Code sizeClass + method + typesParameters → body
Works withAny interface or abstract classFunctional interfaces only
this meansThe anonymous objectThe enclosing object
Can have fields/stateYesNo (only captured values)
Compiled toIts own .class file (Main$1)A private method + invokedynamic

Exam tip: Syntax: (parameters) -> expression or (parameters) -> { statements; }. A lambda can only be used where a functional interface is expected, and it can only use local variables that are effectively final.

3 Types and ways to use it

Lambda syntax, all forms

() -> System.out.println("hi")            // no parameters
x -> x * 2                                // one parameter: brackets optional
(a, b) -> a + b                           // two parameters
(int a, int b) -> a + b                   // explicit types (all or none)
(a, b) -> {                               // block body: braces + return
    int s = a + b;
    return s;
}

Expression lambda

One expression, value returned automatically: p -> p * 1.18.

most common

Block lambda

Several statements in braces, with an explicit return if it returns a value.

{ … }

No-parameter lambda

() -> sendSms(): used for Runnable tasks and callbacks.

Runnable

Method reference

String::toUpperCase, a shorter form when the lambda only calls one method (Lesson 14.2).

::

A lambda has no this of its own

public class Main {
    String name = "outer object";

    void demo() {
        Runnable lambda = () -> System.out.println("lambda sees: " + this.name);
        Runnable anon = new Runnable() {
            String name = "anonymous object";
            public void run() {
                System.out.println("anonymous sees: " + this.name);
            }
        };
        lambda.run();
        anon.run();
    }

    public static void main(String[] args) {
        new Main().demo();
    }
}
lambda sees: outer object anonymous sees: anonymous object

Captured variables must not change. A lambda stores a copy of each local variable it uses. If the variable could change afterwards, the copy would silently be wrong, so the compiler requires it to be effectively final. To count inside a lambda, use the list's size, a stream's count(), or an AtomicInteger.

4 Step by step

What happens at list.sort((a, b) -> a - b)

  1. The compiler finds the target type

    List.sort expects a Comparator<Integer>. Its one abstract method is int compare(Integer, Integer), so a and b are Integers and the body must give an int.
  2. The body becomes a hidden method

    javac moves a - b into a private static method like lambda$main$0(Integer a, Integer b) inside your class.
  3. invokedynamic makes an object

    At the lambda's position, an invokedynamic instruction asks the JVM (once) to generate a tiny class implementing Comparator, and returns an instance of it.
  4. sort calls the object

    sort calls compare(x, y) on that object whenever it needs to order two elements; the call is forwarded to lambda$main$0.
  5. Captured values travel with the object

    If the lambda uses a local variable (like budget), the generated object gets a field holding a copy of its value at the moment the lambda was created.
5 See it happen

From anonymous class to lambda: both views

Left: the code shrinking from an anonymous class to a one-liner. Right: what the compiler and JVM build so the lambda can run.

From anonymous class to lambda, both views 6 steps
What you see prices.sort(new Comparator<Integer>() { public int compare(Integer a, Integer b) { return a - b; } }); prices.sort(new Comparator<Integer>() { public int compare(Integer a, Integer b) { return a - b; } }); prices.sort((Integer a, Integer b) -> { return a - b; }); prices.sort((a, b) -> { return a - b; }); prices.sort((a, b) -> a - b); prices.sort((a, b) -> a - b); boilerplate: class, method name, types types inferred from List one expression: no braces, no return 5 lines 5 lines 3 lines 3 lines 1 line 1 line prices: [450, 120, 899] [120, 450, 899] same behaviour in every version sorted cheapest first The old way: an anonymous class. Most of it is ceremony. Keep only parameters and body. Drop the parameter types. Single expression: drop braces and return. Run it: sorted. myphatshala.in
Behind the screen Main.class (compiled) main(): invokedynamic private static int lambda$main$0(a, b) { return a - b; } HEAP #1 lambda (Comparator) #2 ArrayList [3] call path sort → #1.compare(450, 120) → lambda$main$0 → 330 → many compares → sorted the first time this line runs: the JVM builds a tiny class …and one object of it (#1) no captured values: the same #1 object can be reused every time The body is compiled to a method. invokedynamic at the lambda line. A Comparator object appears. sort calls compare on it. Called once per comparison. Stateless lambdas are reused. myphatshala.in
6 Real-world use

Lambdas in real systems

Modern Java code uses lambdas everywhere behaviour is passed around: sorting, filtering, callbacks, tasks and stream pipelines.

Watch it with an example: sorting restaurants in a food app

The user taps “Sort by rating”, then “Within 3 km”. Each button simply passes a different lambda to the same list methods.

One list, different lambdas 5 steps
Animated example Dosa Corner ★4.1 2.5 km Spice Hub ★4.6 5.0 km Chai Point ★3.9 1.2 km Biryani Bay ★4.4 2.8 km Spice Hub ★4.6 5.0 km Biryani Bay ★4.4 2.8 km Dosa Corner ★4.1 2.5 km Chai Point ★3.9 1.2 km Spice Hub ★4.6 5.0 km Biryani Bay ★4.4 2.8 km Dosa Corner ★4.1 2.5 km Chai Point ★3.9 1.2 km Biryani Bay ★4.4 2.8 km Dosa Corner ★4.1 2.5 km Chai Point ★3.9 1.2 km Biryani Bay ★4.4 2.8 km Dosa Corner ★4.1 2.5 km Chai Point ★3.9 1.2 km sort: (a, b) -> Double .compare(b.rating, a.rating) removeIf: r -> r.km > maxKm maxKm = 3 (from the filter chip) Showing 3 restaurants, best rated first The list from the server. Sort by rating: highest first. Filter: farther than 3 km? Spice Hub removed. Shown to the user. myphatshala.in

1. Sorting and filtering screens

Product lists, transactions and search results are sorted and filtered with comparator and predicate lambdas.

“Price: low to high”→sort((a, b) -> …)→sorted list
Pay

2. Event handlers and callbacks

Button clicks, payment results and HTTP responses are handled by lambdas registered ahead of time.

payment success→() -> showReceipt()→receipt screen

3. Background tasks

Tasks submitted to thread pools are lambdas (Lesson 12.6): pool.submit(() -> sendInvoice(id)).

order placed→submit(() -> …)→worker thread

4. Data pipelines

Reports and analytics chain lambdas through the Stream API (Lesson 14.3): filter, transform, total.

10 lakh orders→filter / map / sum→daily revenue

Simplified: real apps usually sort and filter a copy (or a stream) so the original data stays untouched.

7 Run the code

Sort, filter and print a menu

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> dishes = new ArrayList<>(List.of("thali", "dosa", "chai", "biryani", "idli"));

        dishes.sort((a, b) -> a.compareTo(b));
        System.out.println("A-Z:        " + dishes);

        dishes.sort((a, b) -> b.length() - a.length());
        System.out.println("longest:    " + dishes);

        int maxLen = 4;
        dishes.removeIf(d -> d.length() > maxLen);
        System.out.println("short ones: " + dishes);

        dishes.forEach(d -> System.out.println("  - " + d));
    }
}
A-Z: [biryani, chai, dosa, idli, thali] longest: [biryani, thali, chai, dosa, idli] short ones: [chai, dosa, idli] - chai - dosa - idli
  • (a, b) -> a.compareTo(b)Alphabetical order: a Comparator lambda.
  • (a, b) -> b.length() - a.length()b before a: longest names first.
  • d -> d.length() > maxLenA Predicate lambda that captures maxLen.
  • d -> System.out.println(...)A Consumer lambda called for each element.
8 Line by line

Three lambdas on a price list

Recorded from the real JVM. Each time a library method calls your lambda, a “lambda body” frame appears on the stack. Watch the lambda objects appear on the heap.

Worked examples: real problems, solved and traced in memory

Three ways lambdas replace whole classes in everyday code: a comparator for sorting, behaviour stored in variables, and a list of tasks. Watch the lambda bodies run as separate frames on the stack.

Example 1E-commerce · orders

Sort orders by amount, then by customer

Problem statement

An admin panel lists orders. Sort them by amount (highest first); when amounts are equal, by customer name A–Z. Use a lambda comparator instead of writing a Comparator class.

Input
Ravi 1200, Asha 450, Meena 1200, Imran 99
Expected output
Meena Rs 1200
Ravi Rs 1200
Asha Rs 450
Imran Rs 99

Approach

  1. A small class Order with name and amount.
  2. orders.sort((x, y) -> …): compare amounts in reverse; if equal, compare names.
  3. Print the sorted list.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 2Retail · offers

Discount rules as lambdas

Problem statement

A shop runs different discount rules: festival (20% off), student (Rs 50 off above Rs 300), none. Store each rule as a lambda in a variable of a small interface Rule, and apply all three to a Rs 800 bill.

Input
bill Rs 800
Expected output
festival: Rs 640.0
student:  Rs 750.0
none:     Rs 800.0

Approach

  1. Declare interface Rule { double apply(double bill); }: one abstract method, so lambdas fit.
  2. Assign three lambdas to three Rule variables.
  3. Call apply on each: same call, different behaviour.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 3Notifications · tasks

A queue of tasks as Runnable lambdas

Problem statement

After an order is placed, the app must send an SMS, an email and update stock. Keep these as Runnable lambdas in a list and run them one by one, capturing the order id.

Input
order id 7781
Expected output
SMS: order 7781 confirmed
Email: invoice for 7781
Stock: reserved items of 7781
3 tasks done

Approach

  1. int orderId = 7781; is effectively final, so the lambdas can capture it.
  2. Put three Runnable lambdas in a List.
  3. Loop and call run() on each.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

9 Quiz and revision

Check yourself

Quick revision

Lesson 14.1 in one screen

  • Lambda = anonymous function: (params) -> expression or (params) -> { statements }.
  • It implements the one abstract method of a functional interface; types are inferred.
  • One parameter: brackets optional. No parameters: (). Block body: use return.
  • Captured locals must be effectively final; the lambda keeps a copy.
  • this inside a lambda is the enclosing object.
  • Compiled to a private method + invokedynamic; at run time it's an object.
  • Useful list methods: sort, removeIf, replaceAll, forEach.

Interview and viva questions

What is a lambda expression and why was it added?

An anonymous function that implements a functional interface concisely. Added in Java 8 to make passing behaviour easy (callbacks, comparators, predicates), enabling the Stream API and a more functional style without verbose anonymous classes.

Why must captured variables be effectively final?

A lambda may run later, even on another thread, after the method has returned and its stack frame is gone. So it captures a copy of the value. If the variable could change, the copy and the variable would disagree; requiring effective finality prevents that confusion and race conditions.

Lambda vs anonymous class: what's different?

Lambdas only implement functional interfaces, have no state of their own, don't create a new this scope, and are compiled to a private method linked through invokedynamic rather than a separate class file. Anonymous classes can implement any interface or extend a class, have fields, and this refers to the anonymous object.

How does the JVM execute a lambda?

javac compiles the body into a private (often static) synthetic method and emits an invokedynamic call site. On first execution, the bootstrap method (LambdaMetafactory) generates a hidden class implementing the interface; subsequent executions reuse the linked call site. Non-capturing lambdas can reuse a single instance.

Can a lambda throw a checked exception?

Only if the functional interface's method declares it. Runnable.run() doesn't, so a Runnable lambda must catch checked exceptions itself; Callable.call() declares throws Exception, so a Callable lambda may throw.

Practice problems

Write lambdas for sorting, filtering, looping and your own one-method interfaces. Every output is fixed. The last three are business tasks.

P1Easy

Sort names by length

Sort a list of city names from shortest to longest using a lambda.

Input
Mumbai, Pune, Bengaluru, Goa
Expected output
[Goa, Pune, Mumbai, Bengaluru]
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> cities = new ArrayList<>(List.of("Mumbai", "Pune", "Bengaluru", "Goa"));
        cities.sort((a, b) -> a.length() - b.length());
        System.out.println(cities);
    }
}
[Goa, Pune, Mumbai, Bengaluru]

How it works: a comparator returns negative, zero or positive. List.sort is stable, so equal-length names keep their order.

P2Easy

forEach with a lambda

Print each item of a shopping list in upper case using forEach.

Input
milk, bread, eggs
Expected output
MILK
BREAD
EGGS
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> items = List.of("milk", "bread", "eggs");
        items.forEach(item -> System.out.println(item.toUpperCase()));
    }
}
MILK BREAD EGGS

How it works: forEach calls the lambda once per element. A single parameter needs no brackets.

P3Easy

Your own functional interface

Declare interface Greeter { String greet(String name); } and implement it with a lambda.

Input
Asha
Expected output
Namaste, Asha!
Show solution and explanation
interface Greeter {
    String greet(String name);
}

public class Main {
    public static void main(String[] args) {
        Greeter hello = name -> "Namaste, " + name + "!";
        System.out.println(hello.greet("Asha"));
    }
}
Namaste, Asha!

How it works: any interface with exactly one abstract method can be implemented by a lambda. The lambda's parameter and return types come from that method.

P4Medium

Remove failed payments

Remove all negative amounts (failed payments) from a list with removeIf.

Input
[500, -1, 230, -1, 80]
Expected output
[500, 230, 80]
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Integer> payments = new ArrayList<>(List.of(500, -1, 230, -1, 80));
        payments.removeIf(p -> p < 0);
        System.out.println(payments);
    }
}
[500, 230, 80]

How it works: removeIf is safer than removing inside a for-each loop, which throws ConcurrentModificationException (Lesson 11.7).

P5Medium

Capture a threshold

Keep only marks at or above a pass mark stored in a local variable.

Input
pass 40; marks 35, 72, 40, 18, 90
Expected output
passed: [72, 40, 90]
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        int passMark = 40;
        List<Integer> marks = new ArrayList<>(List.of(35, 72, 40, 18, 90));
        marks.removeIf(m -> m < passMark);
        System.out.println("passed: " + marks);
    }
}
passed: [72, 40, 90]

How it works: the lambda captures passMark. It's never reassigned, so it's effectively final.

P6Medium

Replace every value

Add 18% GST to every price in a list in place, using replaceAll.

Input
[100, 250, 40]
Expected output
[118, 295, 47]
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Integer> prices = new ArrayList<>(List.of(100, 250, 40));
        prices.replaceAll(p -> p * 118 / 100);
        System.out.println(prices);
    }
}
[118, 295, 47]

How it works: replaceAll calls the lambda for each element and stores what it returns. Integer arithmetic truncates: 40 × 118 / 100 = 47.

P7Business problem

Leaderboard sort

Sort sales agents by deals closed (most first), and by name when tied. Print rank, name and deals.

Input
Ravi 12, Asha 15, Meena 12, Joseph 9
Expected output
1. Asha - 15
2. Meena - 12
3. Ravi - 12
4. Joseph - 9
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        String[] names = {"Ravi", "Asha", "Meena", "Joseph"};
        int[] deals = {12, 15, 12, 9};
        List<Integer> idx = new ArrayList<>(List.of(0, 1, 2, 3));
        idx.sort((a, b) -> deals[a] != deals[b] ? deals[b] - deals[a] : names[a].compareTo(names[b]));
        for (int r = 0; r < idx.size(); r++) {
            int i = idx.get(r);
            System.out.println((r + 1) + ". " + names[i] + " - " + deals[i]);
        }
    }
}
1. Asha - 15 2. Meena - 12 3. Ravi - 12 4. Joseph - 9

How it works: sorting a list of indexes lets you sort parallel arrays together. The lambda reads the arrays it captured.

P8Business problem

Validation rules list

A signup form has rules stored as lambdas of interface Check { boolean ok(String phone); }. Run every rule on a phone number and print which ones fail.

Input
phone "98765x210"
Expected output
FAIL 10 digits
FAIL only numbers
ok   starts 6-9
Show solution and explanation
import java.util.*;

interface Check {
    boolean ok(String phone);
}

public class Main {
    public static void main(String[] args) {
        Map<String, Check> rules = new LinkedHashMap<>();
        rules.put("10 digits", p -> p.length() == 10);
        rules.put("only numbers", p -> p.chars().allMatch(Character::isDigit));
        rules.put("starts 6-9", p -> p.charAt(0) >= '6' && p.charAt(0) <= '9');
        String phone = "98765x210";
        rules.forEach((name, rule) -> System.out.println((rule.ok(phone) ? "ok   " : "FAIL ") + name));
    }
}
FAIL 10 digits FAIL only numbers ok starts 6-9

How it works: rules stored as data can be added or removed without changing the checking loop. Map.forEach takes a two-parameter lambda.

P9Business problem

Event handlers for a payment

Register handlers (Runnables) for SUCCESS and FAILED payment events in a map, then fire the handler for an incoming status.

Input
status SUCCESS
Expected output
Show receipt and send SMS
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Map<String, Runnable> handlers = new HashMap<>();
        handlers.put("SUCCESS", () -> System.out.println("Show receipt and send SMS"));
        handlers.put("FAILED", () -> System.out.println("Show retry button"));
        String status = "SUCCESS";
        handlers.getOrDefault(status, () -> System.out.println("Unknown status")).run();
    }
}
Show receipt and send SMS

How it works: this is how button clicks and payment callbacks are wired in real apps: the code to run later is passed as a lambda now.

Real-world scenario questions (asked in company interviews)

Lambdas are in almost every code review today; interviewers check that you can read them and spot their traps.

Code review · sortingFresher

Big amounts sorted in the wrong order

Transactions are sorted with (a, b) -> a.amountPaise - b.amountPaise on int fields. For very large and very negative values (refunds), the order is sometimes wrong. Why?

The interviewer is checking: integer overflow in comparators.

Show a strong answer
  • Subtracting two ints can overflow (e.g. 2,000,000,000 − (−2,000,000,000)), flipping the sign of the result.
  • Use Integer.compare(a.amountPaise, b.amountPaise) or Comparator.comparingInt(T::getAmountPaise).

Avoid: “subtraction is fine for numbers”; it's only safe for small ranges.

Code review · countingFresher

“Variable used in lambda should be final”

A colleague wants to count failed payments with int failed = 0; payments.forEach(p -> { if (p.failed()) failed++; }); and asks how to silence the compiler error. What do you suggest?

The interviewer is checking: effectively final and idiomatic alternatives.

Show a strong answer
  • Captured locals can't be modified. Don't fight it: use payments.stream().filter(Payment::failed).count(), or a plain for-each loop.
  • Tricks like an int[] or AtomicInteger work but hide intent, and an int[] is unsafe if the lambda runs on several threads.

Avoid: making failed a static field just to compile; that introduces shared mutable state.

Debugging · stack tracesJunior

A stack trace full of lambda$ frames

Production logs show NullPointerException at OrderService.lambda$process$3(OrderService.java:88). A teammate says “lambdas make debugging impossible”. How do you read this trace?

The interviewer is checking: knowing how lambdas compile.

Show a strong answer
  • lambda$process$3 is the synthetic method for the fourth lambda in process(); line 88 is inside its body.
  • Open line 88, see which value can be null (often an element or a field of the element), and handle it.
  • If a lambda grows long, extract it into a named method and use a method reference: clearer traces and easier testing.

Avoid: rewriting everything as anonymous classes; the trace would just say OrderService$4.

Design · readabilityMid

A 40-line lambda

A pull request passes a 40-line block lambda with nested ifs and try/catch into forEach. It works. Would you approve it?

The interviewer is checking: judgement about when lambdas help.

Show a strong answer
  • Lambdas are best for short, clear behaviour. A 40-line body hides a real piece of logic without a name, tests or reuse.
  • Extract it into a well-named private method (reconcile(Payment p)) and pass this::reconcile; or use a normal loop if it has side effects and early exits.

Avoid: “lambdas are modern, so longer lambdas are better”.