Java Lambda Expressions
A complete guide to Java Lambda Expressions — syntax variations, functional interfaces, all built-in functional types (Predicate, Function, Consumer, Supplier, BiFunction), method references, variable capture rules, lambda with collections and Streams API, and best practices with real-world examples.
Last Updated
March 2026
Read Time
19 min
Level
Beginner to Intermediate
What is a Lambda Expression in Java?
A Lambda Expression is a concise, anonymous block of code — a function without a name — that can be passed as an argument, stored in a variable, or returned from a method, just like a value. Introduced in Java 8, lambdas brought functional programming capabilities into the Java language for the first time.
Before Java 8, passing behavior (a piece of logic) required creating an anonymous inner class — verbose, boilerplate-heavy code that obscured intent. Lambda expressions eliminate this boilerplate entirely. A lambda implements exactly one abstract method of a functional interface, allowing you to treat behavior as data and write expressive, readable code.
Lambda expressions are the foundation of modern Java — they power the Streams API, enable clean event handling, simplify sorting and filtering of collections, and make concurrency patterns like CompletableFuture readable. Mastering lambdas is non-negotiable for any Java developer working with Java 8 and above.
import java.util.*;
public class LambdaIntro {
public static void main(String[] args) {
List<String> names = Arrays.asList("Priya", "Rahul", "Amit", "Neha");
// ❌ BEFORE Java 8 — Anonymous Inner Class (verbose)
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
// ✅ AFTER Java 8 — Lambda Expression (concise)
Collections.sort(names, (a, b) -> a.compareTo(b));
// ✅ Even cleaner — Method Reference
names.sort(String::compareTo);
System.out.println(names); // [Amit, Neha, Priya, Rahul]
}
}Output
[Amit, Neha, Priya, Rahul]Lambda Syntax — All Variations
Lambda syntax in Java is flexible. Parameters, parentheses, braces, and return statements can each be omitted under specific conditions. Understanding all the shorthand forms prevents confusion when reading real-world code.
// General form:
// (parameter list) -> { body }
// ── NO PARAMETERS ──────────────────────────────────────────────
Runnable r = () -> System.out.println("Running!");
// ── ONE PARAMETER — parentheses optional ────────────────────────
Consumer<String> print1 = s -> System.out.println(s); // no parens
Consumer<String> print2 = (s) -> System.out.println(s); // with parens (also valid)
// ── ONE PARAMETER WITH TYPE (parens required when type is explicit) ──
Consumer<String> print3 = (String s) -> System.out.println(s);
// ── TWO PARAMETERS — parentheses always required ────────────────
Comparator<Integer> cmp = (a, b) -> a - b;
// ── EXPRESSION BODY — no braces, return is implicit ─────────────
Function<Integer, Integer> square = n -> n * n;
// equivalent to: n -> { return n * n; }
// ── BLOCK BODY — braces required, return must be explicit ───────
Function<Integer, String> grade = score -> {
if (score >= 90) return "A";
if (score >= 75) return "B";
return "C";
};
// ── VOID BLOCK BODY — no return needed ──────────────────────────
BiConsumer<String, Integer> log = (msg, level) -> {
System.out.println("[L" + level + "] " + msg);
};
// Usage
System.out.println(square.apply(7)); // 49
System.out.println(grade.apply(88)); // B
log.accept("Server started", 1); // [L1] Server startedOutput
49 B [L1] Server startedFunctional Interfaces — The Foundation of Lambdas
A Functional Interface is an interface with exactly one abstract method (SAM — Single Abstract Method). This is the contract that a lambda implements. An interface can have any number of default and static methods alongside its single abstract method — only abstract methods count toward the SAM rule.
// @FunctionalInterface annotation is optional but strongly recommended.
// It instructs the compiler to enforce the single-abstract-method rule.
@FunctionalInterface
interface Validator<T> {
boolean validate(T value); // the single abstract method
// default and static methods are allowed — do NOT count as abstract
default Validator<T> and(Validator<T> other) {
return value -> this.validate(value) && other.validate(value);
}
static <T> Validator<T> alwaysTrue() {
return value -> true;
}
}
public class FunctionalInterfaceDemo {
public static void main(String[] args) {
Validator<String> notEmpty = s -> !s.isEmpty();
Validator<String> notTooLong = s -> s.length() <= 20;
// Compose validators using default method
Validator<String> usernameRule = notEmpty.and(notTooLong);
System.out.println(usernameRule.validate("rahul_dev")); // true
System.out.println(usernameRule.validate("")); // false
System.out.println(usernameRule.validate("this_username_is_way_too_long")); // false
}
}Output
true false falseImportant: If you add a second abstract method to a @FunctionalInterface, the compiler immediately reports an error: 'Invalid '@FunctionalInterface' annotation; Validator is not a functional interface'. This compile-time guard is the primary value of the annotation.
Built-in Functional Interfaces — java.util.function Package
Java 8 introduced the java.util.function package with 43 built-in functional interfaces covering the most common patterns. You rarely need to write your own functional interface — one of these almost always fits. Here are the core ones you must know:
Predicate<T> — Testing Conditions
Predicate<T> represents a condition that takes one argument and returns true or false. It is the go-to interface for filtering and validation. Predicates can be composed using and(), or(), and negate() default methods.
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class PredicateDemo {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(3, 7, 12, 18, 25, 6, 41, 30);
// Basic Predicates
Predicate<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isAbove10 = n -> n > 10;
// and() — both conditions must be true
Predicate<Integer> evenAndAbove10 = isEven.and(isAbove10);
// or() — at least one condition must be true
Predicate<Integer> evenOrAbove10 = isEven.or(isAbove10);
// negate() — reverses the condition
Predicate<Integer> isOdd = isEven.negate();
System.out.print("Even AND > 10: ");
numbers.stream().filter(evenAndAbove10).forEach(n -> System.out.print(n + " "));
System.out.print("\nOdd numbers: ");
numbers.stream().filter(isOdd).forEach(n -> System.out.print(n + " "));
// Predicate.not() — Java 11 static helper
List<String> cities = Arrays.asList("Delhi", "", "Mumbai", "", "Pune");
System.out.print("\nNon-empty cities: ");
cities.stream().filter(Predicate.not(String::isEmpty))
.forEach(c -> System.out.print(c + " "));
}
}Output
Even AND > 10: 12 18 30 Odd numbers: 3 7 25 41 Non-empty cities: Delhi Mumbai PuneFunction<T,R> and BiFunction<T,U,R> — Transforming Data
Function<T,R> transforms a value of type T into a value of type R. It is the backbone of mapping operations. Functions can be chained using andThen() (apply this, then the next) and compose() (apply the other first, then this).
import java.util.function.*;
public class FunctionDemo {
public static void main(String[] args) {
// Basic Function — String to Integer
Function<String, Integer> strLength = String::length;
System.out.println(strLength.apply("Lambda")); // 6
// Chaining with andThen()
// Step 1: multiply by 2 → Step 2: convert to String
Function<Integer, Integer> doubleIt = n -> n * 2;
Function<Integer, String> toMessage = n -> "Result: " + n;
Function<Integer, String> pipeline = doubleIt.andThen(toMessage);
System.out.println(pipeline.apply(15)); // Result: 30
// compose() — reverse order: toUpperCase BEFORE addPrefix
Function<String, String> addPrefix = s -> "Dr. " + s;
Function<String, String> toUpperCase = String::toUpperCase;
Function<String, String> titleCase = addPrefix.compose(toUpperCase);
System.out.println(titleCase.apply("rahul")); // Dr. RAHUL
// Function.identity() — returns the input unchanged
Function<String, String> identity = Function.identity();
System.out.println(identity.apply("unchanged")); // unchanged
// BiFunction — two inputs, one output
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
System.out.println(repeat.apply("Ha", 3)); // HaHaHa
}
}Output
6 Result: 30 Dr. RAHUL unchanged HaHaHaConsumer<T> and Supplier<T>
Consumer<T> performs an action on a value and returns nothing (void). It is used for side effects like printing, logging, saving to a database, or sending an email. Consumer can be chained with andThen().
Supplier<T> takes no input and returns a value. It represents a factory or lazy value provider — the value is only computed when get() is called. This is ideal for deferred initialization and optional default values.
import java.util.function.*;
import java.util.*;
public class ConsumerSupplierDemo {
public static void main(String[] args) {
// ── CONSUMER ───────────────────────────────────────────────────
Consumer<String> printLine = s -> System.out.println(s);
Consumer<String> printUpper = s -> System.out.println(s.toUpperCase());
// andThen() — chain consumers: run printLine THEN printUpper
Consumer<String> bothActions = printLine.andThen(printUpper);
bothActions.accept("lambda is powerful");
// Output:
// lambda is powerful
// LAMBDA IS POWERFUL
System.out.println("---");
// BiConsumer — consuming two inputs
BiConsumer<String, Double> printPrice =
(item, price) -> System.out.printf("%-15s ₹%.2f%n", item, price);
printPrice.accept("Laptop", 75999.00);
printPrice.accept("Keyboard", 1299.00);
System.out.println("---");
// ── SUPPLIER ───────────────────────────────────────────────────
// Lazy greeting — only computed when get() is called
Supplier<String> greeting = () -> "Hello, World! Time: " + new Date();
System.out.println(greeting.get());
// Supplier as lazy default in Optional
Optional<String> maybeName = Optional.empty();
String name = maybeName.orElseGet(() -> "Guest");
System.out.println("Welcome, " + name);
// Supplier as a factory
Supplier<List<String>> listFactory = ArrayList::new;
List<String> list1 = listFactory.get();
List<String> list2 = listFactory.get();
list1.add("Java");
System.out.println("list1: " + list1 + ", list2: " + list2);
}
}Output
lambda is powerful LAMBDA IS POWERFUL --- Laptop ₹75999.00 Keyboard ₹1299.00 --- Hello, World! Time: Sun Mar 16 10:00:00 IST 2026 Welcome, Guest list1: [Java], list2: []Method References — All Four Types
A Method Reference is a shorthand lambda that delegates directly to an existing method using the :: operator. It makes code significantly more readable when the lambda body is just a single method call. There are exactly four types of method references in Java.
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class MethodReferenceDemo {
public static void main(String[] args) {
List<String> names = Arrays.asList("priya", "RAHUL", "Amit", "neha");
// ── TYPE 1: Static Method Reference ────────────────────────────
// Lambda: s -> Integer.parseInt(s)
// Ref: Integer::parseInt
List<String> numStrs = Arrays.asList("10", "20", "30");
List<Integer> nums = numStrs.stream()
.map(Integer::parseInt) // static method ref
.collect(Collectors.toList());
System.out.println("Parsed: " + nums); // [10, 20, 30]
// ── TYPE 2: Instance Method on Specific Object ──────────────────
// Lambda: s -> System.out.println(s)
// Ref: System.out::println
names.forEach(System.out::println); // prints each name
// ── TYPE 3: Instance Method on Arbitrary Type Instance ──────────
// Lambda: s -> s.toUpperCase()
// Ref: String::toUpperCase
List<String> upper = names.stream()
.map(String::toUpperCase) // instance method ref on type
.collect(Collectors.toList());
System.out.println("Upper: " + upper);
// ── TYPE 4: Constructor Reference ───────────────────────────────
// Lambda: s -> new StringBuilder(s)
// Ref: StringBuilder::new
List<StringBuilder> builders = names.stream()
.map(StringBuilder::new) // constructor ref
.collect(Collectors.toList());
builders.forEach(sb -> System.out.println(sb.reverse()));
}
}Output
Parsed: [10, 20, 30] priya RAHUL Amit neha Upper: [PRIYA, RAHUL, AMIT, NEHA] ayirp LUHAR timA ahenVariable Capture and the Effectively Final Rule
A lambda can capture (reference) variables from its enclosing scope, but with one critical restriction: captured local variables must be final or effectively final. A variable is effectively final if its value is never reassigned after initialization — even without the explicit final keyword.
import java.util.function.*;
public class VariableCaptureDemo {
private String instanceField = "instance"; // ✅ Instance fields are always capturable
private static String staticField = "static"; // ✅ Static fields are always capturable
public void demonstrate() {
String city = "Mumbai"; // effectively final — never reassigned
int taxRate = 18; // effectively final
// ✅ VALID — city and taxRate are effectively final
Function<Double, String> invoiceFormatter =
price -> String.format("%s | Tax: %.0f%% | Total: ₹%.2f",
city, (double) taxRate, price * (1 + taxRate / 100.0));
System.out.println(invoiceFormatter.apply(5000.0));
// ✅ VALID — instance and static fields can always be captured
Supplier<String> s = () -> instanceField + " + " + staticField;
System.out.println(s.get());
// ❌ COMPILE ERROR — counter is NOT effectively final (it is reassigned)
// int counter = 0;
// Runnable r = () -> System.out.println(counter++); // ERROR
// ✅ WORKAROUND — use an array or AtomicInteger for mutable state
int[] counter = {0};
Runnable increment = () -> counter[0]++; // array itself is final
increment.run();
increment.run();
System.out.println("Counter: " + counter[0]); // 2
}
public static void main(String[] args) {
new VariableCaptureDemo().demonstrate();
}
}Output
Mumbai | Tax: 18% | Total: ₹5900.00 instance + static Counter: 2Why this restriction exists: Local variables live on the thread's stack. A lambda may execute on a different thread or outlive the method that created it. If a local variable were mutable and captured by a lambda, concurrent modifications could corrupt state unpredictably. The effectively-final rule makes lambda capture safe without requiring explicit synchronization.
Lambda vs Anonymous Inner Class
While lambdas replaced anonymous inner classes for functional interface usage, they are not identical. Understanding their differences prevents subtle bugs.
public class ThisReference {
String name = "ThisReference";
void show() {
// Anonymous class — 'this' is the anon class instance
Runnable anon = new Runnable() {
String name = "AnonymousClass";
@Override
public void run() {
System.out.println("Anon 'this': " + this.name); // AnonymousClass
}
};
// Lambda — 'this' is the enclosing ThisReference instance
Runnable lambda = () -> {
System.out.println("Lambda 'this': " + this.name); // ThisReference
};
anon.run();
lambda.run();
}
public static void main(String[] args) {
new ThisReference().show();
}
}Output
Anon 'this': AnonymousClass Lambda 'this': ThisReferenceLambda with Collections
Lambda expressions transformed how Java developers work with collections. The Iterable.forEach(), List.sort(), Map.forEach(), removeIf(), and replaceAll() methods all accept lambdas directly.
import java.util.*;
public class LambdaCollections {
public static void main(String[] args) {
List<String> langs = new ArrayList<>(Arrays.asList(
"Java", "Python", "Go", "Rust", "Kotlin", "C"
));
// forEach — print each element
System.out.println("All: ");
langs.forEach(l -> System.out.print(l + " "));
// sort — alphabetical order
langs.sort((a, b) -> a.compareTo(b));
System.out.println("\nSorted: " + langs);
// sort — by length descending
langs.sort(Comparator.comparingInt(String::length).reversed());
System.out.println("By length desc: " + langs);
// removeIf — remove short names (length <= 2)
langs.removeIf(l -> l.length() <= 2);
System.out.println("After removeIf: " + langs);
// replaceAll — transform all elements
langs.replaceAll(String::toUpperCase);
System.out.println("After replaceAll: " + langs);
// Map forEach
Map<String, Integer> scores = new LinkedHashMap<>();
scores.put("Alice", 92);
scores.put("Bob", 78);
scores.put("Carol", 88);
System.out.println("\nMap forEach:");
scores.forEach((name, score) ->
System.out.printf("%-8s → %d%n", name, score));
// Map computeIfAbsent with lambda
scores.computeIfAbsent("Dave", k -> 75);
System.out.println("After computeIfAbsent: " + scores);
}
}Output
All: Java Python Go Rust Kotlin C Sorted: [C, Go, Java, Kotlin, Python, Rust] By length desc: [Python, Kotlin, Java, Rust, Go, C] After removeIf: [Python, Kotlin, Java, Rust] After replaceAll: [PYTHON, KOTLIN, JAVA, RUST] Map forEach: Alice → 92 Bob → 78 Carol → 88 After computeIfAbsent: {Alice=92, Bob=78, Carol=88, Dave=75}Lambda with Streams API
The Streams API is where lambda expressions truly shine. Streams allow you to express complex data processing pipelines — filter, transform, aggregate — in a declarative, readable way. Every intermediate stream operation accepts a lambda or method reference.
import java.util.*;
import java.util.stream.*;
class Product {
String name;
String category;
double price;
int stock;
Product(String name, String category, double price, int stock) {
this.name = name; this.category = category;
this.price = price; this.stock = stock;
}
public String toString() { return name + "(₹" + price + ")"; }
}
public class StreamLambdaDemo {
public static void main(String[] args) {
List<Product> products = Arrays.asList(
new Product("Laptop", "Electronics", 75000, 10),
new Product("Phone", "Electronics", 25000, 5),
new Product("Desk", "Furniture", 15000, 3),
new Product("Headphones","Electronics", 3000, 20),
new Product("Chair", "Furniture", 8000, 7),
new Product("Tablet", "Electronics", 35000, 0)
);
// 1. Filter Electronics with stock > 0, sort by price desc, get names
System.out.println("In-stock Electronics:");
products.stream()
.filter(p -> p.category.equals("Electronics"))
.filter(p -> p.stock > 0)
.sorted((a, b) -> Double.compare(b.price, a.price))
.map(p -> p.name + " ₹" + p.price)
.forEach(System.out::println);
// 2. Total value of all in-stock products
double totalValue = products.stream()
.filter(p -> p.stock > 0)
.mapToDouble(p -> p.price * p.stock)
.sum();
System.out.printf("%nTotal inventory value: ₹%.0f%n", totalValue);
// 3. Group products by category
Map<String, List<Product>> byCategory = products.stream()
.collect(Collectors.groupingBy(p -> p.category));
System.out.println("\nBy category: " + byCategory);
// 4. Average price per category
System.out.println("\nAverage price by category:");
products.stream()
.collect(Collectors.groupingBy(
p -> p.category,
Collectors.averagingDouble(p -> p.price)))
.forEach((cat, avg) ->
System.out.printf("%-15s ₹%.0f%n", cat, avg));
}
}Output
In-stock Electronics: Laptop ₹75000.0 Phone ₹25000.0 Headphones ₹3000.0 Total inventory value: ₹1076000 By category: {Electronics=[Laptop(₹75000.0), Phone(₹25000.0), Headphones(₹3000.0), Tablet(₹35000.0)], Furniture=[Desk(₹15000.0), Chair(₹8000.0)]} Average price by category: Electronics ₹34500 Furniture ₹11500Lambda Execution Flow — Flowchart
The flowchart below shows how Java resolves and executes a lambda expression at runtime — from the point a lambda is assigned to a variable through its invocation.
Code Execution Flow — from source to output
Best Practices for Writing Lambda Expressions
Lambdas can make code dramatically more readable — or dramatically more unreadable. Following these best practices ensures your lambdas stay clean, maintainable, and professional.
- ▶
✅ 1. Keep Lambdas Short — One Line Ideally — A lambda should ideally be an expression, not a multi-line block. If your lambda exceeds 2-3 lines, extract it into a named private method and reference it. This improves readability and makes the lambda self-documenting through the method name.
- ▶
✅ 2. Use Method References When Possible — Replace s -> s.toUpperCase() with String::toUpperCase. Method references are more concise and directly name the behavior, making the code read like documentation.
- ▶
✅ 3. Prefer Specific Functional Interfaces Over Custom Ones — Before writing a custom @FunctionalInterface, check java.util.function. Predicate, Function, Consumer, Supplier, BiFunction cover 90% of cases. Custom interfaces add unnecessary complexity.
- ▶
✅ 4. Avoid Side Effects in Streams — Lambda expressions inside stream operations (map, filter, flatMap) should be stateless and side-effect-free. Modifying external state inside a stream lambda breaks referential transparency and causes subtle bugs in parallel streams.
- ▶
✅ 5. Never Mutate Captured Variables — Even if you use an int[] workaround to bypass the effectively-final rule, mutating captured state in lambdas is an anti-pattern. It breaks reasoning about code flow and is unsafe in parallel operations. Use AtomicInteger or a reduction operation instead.
- ▶
✅ 6. Add Type Annotations Only When Needed for Clarity — Let the compiler infer parameter types in most cases: (a, b) -> a + b is cleaner than (Integer a, Integer b) -> a + b. Add explicit types only when inference fails or when it significantly aids readability for reviewers.
- ▶
✅ 7. Name Complex Lambdas as Variables — Assign reusable or complex lambdas to descriptive variable names: Predicate<Order> isEligibleForDiscount = o -> o.total > 1000 && o.isPremiumUser. This documents intent and allows the same logic to be reused across multiple stream operations.
- ▶
❌ 8. Do Not Swallow Checked Exceptions in Lambdas — Lambdas cannot throw checked exceptions unless the functional interface declares them. Wrapping checked exceptions in try-catch inside every lambda is verbose. Instead, create a utility wrapper method that converts checked exceptions to unchecked, or use a custom functional interface that declares throws Exception.
Real-World Code Examples
Example 1 — Order Processing Pipeline with Lambdas
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
class Order {
int id;
String customer;
double amount;
String status;
Order(int id, String customer, double amount, String status) {
this.id = id; this.customer = customer;
this.amount = amount; this.status = status;
}
public String toString() {
return "#" + id + "(" + customer + ", ₹" + amount + ", " + status + ")";
}
}
public class OrderProcessor {
// Named Predicates — reusable, self-documenting
static Predicate<Order> isPending = o -> o.status.equals("PENDING");
static Predicate<Order> isHighValue = o -> o.amount > 10000;
// Named Function — transforms Order to invoice string
static Function<Order, String> toInvoice =
o -> String.format("INV-%04d | %s | ₹%.2f", o.id, o.customer, o.amount);
// Named Consumer — simulates sending email
static Consumer<Order> sendConfirmation =
o -> System.out.println("📧 Sending confirmation to: " + o.customer);
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order(1, "Rahul", 15000, "PENDING"),
new Order(2, "Priya", 8000, "SHIPPED"),
new Order(3, "Amit", 22000, "PENDING"),
new Order(4, "Neha", 3000, "PENDING"),
new Order(5, "Vikram", 45000, "DELIVERED")
);
System.out.println("=== High-Value Pending Orders ===");
orders.stream()
.filter(isPending.and(isHighValue))
.peek(sendConfirmation) // side-effect without breaking pipeline
.map(toInvoice)
.forEach(System.out::println);
System.out.println("\n=== Revenue Summary ===");
DoubleSummaryStatistics stats = orders.stream()
.filter(isPending)
.mapToDouble(o -> o.amount)
.summaryStatistics();
System.out.printf("Pending orders: %d%n", stats.getCount());
System.out.printf("Total pending: ₹%.0f%n", stats.getSum());
System.out.printf("Average: ₹%.0f%n", stats.getAverage());
}
}Output
=== High-Value Pending Orders === 📧 Sending confirmation to: Rahul INV-0001 | Rahul | ₹15000.00 📧 Sending confirmation to: Amit INV-0003 | Amit | ₹22000.00 === Revenue Summary === Pending orders: 3 Total pending: ₹40000 Average: ₹13333Example 2 — Strategy Pattern with Lambdas
import java.util.function.*;
// Traditional Strategy Pattern collapses to a single functional interface
@FunctionalInterface
interface DiscountStrategy {
double apply(double originalPrice);
}
class PricingEngine {
private final DiscountStrategy strategy;
PricingEngine(DiscountStrategy strategy) {
this.strategy = strategy;
}
double getFinalPrice(double price) {
return strategy.apply(price);
}
}
public class StrategyLambdaDemo {
public static void main(String[] args) {
double price = 10000.0;
// Each lambda IS a complete strategy implementation
DiscountStrategy noDiscount = p -> p;
DiscountStrategy tenPercent = p -> p * 0.90;
DiscountStrategy flatFiveHundred = p -> Math.max(0, p - 500);
DiscountStrategy premiumRate = p -> p * 0.75;
PricingEngine[] engines = {
new PricingEngine(noDiscount),
new PricingEngine(tenPercent),
new PricingEngine(flatFiveHundred),
new PricingEngine(premiumRate)
};
String[] labels = {"No Discount", "10% Off", "Flat ₹500 Off", "Premium 25%"};
for (int i = 0; i < engines.length; i++) {
System.out.printf("%-20s → ₹%.0f%n",
labels[i], engines[i].getFinalPrice(price));
}
}
}Output
No Discount → ₹10000 10% Off → ₹9000 Flat ₹500 Off → ₹9500 Premium 25% → ₹7500Practice This Code — Live Editor
Advantages and Disadvantages of Lambda Expressions
Lambda expressions transformed Java programming. Understanding both their power and their pitfalls makes you a more effective developer.
Java Lambda Expressions — Interview Questions
Lambda expressions are one of the most heavily tested Java 8 topics in technical interviews at all levels. Master every question below.
Practice Questions — Test Your Knowledge
Test your understanding of Java Lambda Expressions with these practice questions. Attempt each answer before revealing it.
1. What is the output? Function<Integer,Integer> f = x -> x + 10; Function<Integer,Integer> g = x -> x * 2; System.out.println(f.andThen(g).apply(5)); System.out.println(f.compose(g).apply(5));
Medium2. Will this code compile? Why or why not? int multiplier = 3; Function<Integer, Integer> triple = n -> n * multiplier; multiplier = 5;
Easy3. Write a lambda that takes a list of strings and returns a new list with only strings longer than 4 characters, sorted alphabetically and converted to uppercase.
Medium4. What is wrong with this code? List<Integer> numbers = Arrays.asList(1,2,3,4,5); List<Integer> result = new ArrayList<>(); numbers.stream().filter(n -> n % 2 == 0).forEach(n -> result.add(n));
Medium5. Rewrite this anonymous class as a lambda: new Thread(new Runnable() { @Override public void run() { System.out.println("Thread running"); } }).start();
Easy6. Using BiFunction, write a lambda that takes a String name and an int age and returns a formatted greeting: 'Hello, Rahul! You are 25 years old.'
Easy7. Can a lambda implement an interface that has two abstract methods? Why or why not?
Medium8. Given a Map<String, Integer>, write a lambda pipeline that: (a) filters entries where value > 50, (b) sorts by value descending, (c) prints each key-value pair.
HardConclusion — Lambda Expressions: Java's Functional Revolution
Lambda expressions were the single most transformative feature introduced in Java 8. They did not just add a shorthand syntax — they fundamentally changed how Java programs are written. Behavior became a value. Functions became composable. Collections became pipelines. Code that once took a page became a readable chain of expressions.
Mastering lambdas means mastering functional interfaces (the contract), built-in functional types (Predicate, Function, Consumer, Supplier), method references (the shorthand), variable capture rules (the constraint), and stream integration (the payoff). Each concept builds on the last.
Your next steps: dive deep into the Streams API (filter, map, reduce, collect, flatMap, groupingBy), explore Optional for null-safe lambda chaining, and study CompletableFuture for async lambda pipelines. These three topics, combined with lambdas, form the core of modern idiomatic Java. ☕