What are lambda expressions in Java and how do they work?
(params) -> body, enabling you to pass behaviour as a value in Java 8+.Table of Contents
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.
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:
thisinside 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 sortingRunnable— run a task with no parameters/returnFunction<T,R>— transform T into RPredicate<T>— test a condition on TConsumer<T>— process T, return nothingSupplier<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, andjava.util.functiontypes.
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
thisinside a lambda vs. anonymous class — in a lambda,thisis the enclosing class; in an anonymous class,thisis 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.