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.
Sir, the only line that matters is a.price - b.price. Why all this wrapping?
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.
But sort expects a Comparator object. How does a line of code become an object?
The JVM builds the object for you. Let's shrink the class step by step, then look behind the screen.
What is a lambda expression?
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.
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
Anonymous class vs lambda
| Anonymous class | Lambda | |
|---|---|---|
| Code size | Class + method + types | Parameters → body |
| Works with | Any interface or abstract class | Functional interfaces only |
this means | The anonymous object | The enclosing object |
| Can have fields/state | Yes | No (only captured values) |
| Compiled to | Its 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.
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.
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.
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();
}
}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.
What happens at list.sort((a, b) -> a - b)
The compiler finds the target type
List.sortexpects aComparator<Integer>. Its one abstract method isint compare(Integer, Integer), soaandbare Integers and the body must give an int.The body becomes a hidden method
javac movesa - binto a private static method likelambda$main$0(Integer a, Integer b)inside your class.invokedynamic makes an object
At the lambda's position, aninvokedynamicinstruction asks the JVM (once) to generate a tiny class implementing Comparator, and returns an instance of it.sort calls the object
sortcallscompare(x, y)on that object whenever it needs to order two elements; the call is forwarded tolambda$main$0.Captured values travel with the object
If the lambda uses a local variable (likebudget), the generated object gets a field holding a copy of its value at the moment the lambda was created.
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.
- Before Java 8, passing behaviour meant writing an anonymous class: here a Comparator that compares two prices.
- Only one line,
return a - b;, is the real logic. The rest (class, method name, types) is the same every time. - A lambda keeps just the parameters, an arrow
->, and the body. - The compiler already knows the list holds Integers, so it can infer the parameter types.
- When the body is a single expression, drop the braces and
return: the value is returned automatically. sortcalls the lambda each time it needs to compare two prices. The list ends up cheapest first.
- javac compiles the lambda body into a hidden
private staticmethod of Main, named something likelambda$main$0. No separate .class file is written. - Where the lambda appears, javac puts an
invokedynamicinstruction. The first time it runs, the JVM generates a small class that implementsComparator. - invokedynamic returns an instance of that class (#1): the lambda is a real object on the heap.
sortonly knows it has a Comparator. It callscompare(450, 120), which forwards tolambda$main$0.- Sorting calls compare once per comparison; the lambda object just forwards each call.
- A lambda that captures nothing has no state, so the JVM can hand out the same object every time that line runs. A lambda that captures a variable gets a field holding a copy of it.
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.
- The app has a
List<Restaurant>in server order. - “Sort by rating” calls
list.sort(…)with a lambda that compares b with a, so the highest rating comes first. - “Within 3 km” calls
removeIfwith a lambda that usesmaxKm, a local variable the lambda captures. - Spice Hub is 5 km away, so the lambda returns true for it and it's removed.
- No new classes were written for either button: each behaviour was a one-line lambda handed to a library method. (Real apps usually filter a copy so the original list is kept.)
1. Sorting and filtering screens
Product lists, transactions and search results are sorted and filtered with comparator and predicate lambdas.
2. Event handlers and callbacks
Button clicks, payment results and HTTP responses are handled by lambdas registered ahead of time.
3. Background tasks
Tasks submitted to thread pools are lambdas (Lesson 12.6): pool.submit(() -> sendInvoice(id)).
4. Data pipelines
Reports and analytics chain lambdas through the Stream API (Lesson 14.3): filter, transform, total.
Simplified: real apps usually sort and filter a copy (or a stream) so the original data stays untouched.
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, 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 capturesmaxLen.d -> System.out.println(...)A Consumer lambda called for each element.
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.
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.
Ravi 1200, Asha 450, Meena 1200, Imran 99
Meena Rs 1200 Ravi Rs 1200 Asha Rs 450 Imran Rs 99
Approach
- A small class
Orderwith name and amount. orders.sort((x, y) -> …): compare amounts in reverse; if equal, compare names.- 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.
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.
bill Rs 800
festival: Rs 640.0 student: Rs 750.0 none: Rs 800.0
Approach
- Declare
interface Rule { double apply(double bill); }: one abstract method, so lambdas fit. - Assign three lambdas to three
Rulevariables. - Call
applyon 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.
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.
order id 7781
SMS: order 7781 confirmed Email: invoice for 7781 Stock: reserved items of 7781 3 tasks done
Approach
int orderId = 7781;is effectively final, so the lambdas can capture it.- Put three
Runnablelambdas in aList. - 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.
Check yourself
Quick revision
Lesson 14.1 in one screen
- Lambda = anonymous function:
(params) -> expressionor(params) -> { statements }. - It implements the one abstract method of a functional interface; types are inferred.
- One parameter: brackets optional. No parameters:
(). Block body: usereturn. - Captured locals must be effectively final; the lambda keeps a copy.
thisinside 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.
Sort names by length
Sort a list of city names from shortest to longest using a lambda.
Mumbai, Pune, Bengaluru, Goa
[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);
}
}How it works: a comparator returns negative, zero or positive. List.sort is stable, so equal-length names keep their order.
forEach with a lambda
Print each item of a shopping list in upper case using forEach.
milk, bread, eggs
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()));
}
}How it works: forEach calls the lambda once per element. A single parameter needs no brackets.
Your own functional interface
Declare interface Greeter { String greet(String name); } and implement it with a lambda.
Asha
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"));
}
}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.
Remove failed payments
Remove all negative amounts (failed payments) from a list with removeIf.
[500, -1, 230, -1, 80]
[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);
}
}How it works: removeIf is safer than removing inside a for-each loop, which throws ConcurrentModificationException (Lesson 11.7).
Capture a threshold
Keep only marks at or above a pass mark stored in a local variable.
pass 40; marks 35, 72, 40, 18, 90
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);
}
}How it works: the lambda captures passMark. It's never reassigned, so it's effectively final.
Replace every value
Add 18% GST to every price in a list in place, using replaceAll.
[100, 250, 40]
[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);
}
}How it works: replaceAll calls the lambda for each element and stores what it returns. Integer arithmetic truncates: 40 × 118 / 100 = 47.
Leaderboard sort
Sort sales agents by deals closed (most first), and by name when tied. Print rank, name and deals.
Ravi 12, Asha 15, Meena 12, Joseph 9
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]);
}
}
}How it works: sorting a list of indexes lets you sort parallel arrays together. The lambda reads the arrays it captured.
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.
phone "98765x210"
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));
}
}How it works: rules stored as data can be added or removed without changing the checking loop. Map.forEach takes a two-parameter lambda.
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.
status SUCCESS
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();
}
}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.
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)orComparator.comparingInt(T::getAmountPaise).
Avoid: “subtraction is fine for numbers”; it's only safe for small ranges.
“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[]orAtomicIntegerwork 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.
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$3is the synthetic method for the fourth lambda inprocess(); 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.
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 passthis::reconcile; or use a normal loop if it has side effects and early exits.
Avoid: “lambdas are modern, so longer lambdas are better”.