Imagine you need to give someone quick instructions. You could write a full manual with a title page, table of contents, and chapters — or you could just hand them a sticky note: “Sort these by price, lowest first.” A lambda expression is that sticky note. It is a concise way to represent a small piece of behavior — a function — without the ceremony of defining an entire class or method.
Introduced in Java 8, lambda expressions bring functional programming capabilities to Java. Before Java 8, every piece of behavior had to live inside a class. If you wanted to pass a comparator to a sort method, you had to create an anonymous inner class with boilerplate code. Lambdas eliminate that boilerplate.
Formally defined: A lambda expression is an anonymous function — a function with no name, no access modifier, and no return type declaration. It provides a clear and concise way to implement a single abstract method of a functional interface.
What lambdas give you:
Here is a before-and-after comparison to see the difference immediately:
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class LambdaBeforeAfter {
public static void main(String[] args) {
List names = Arrays.asList("Charlie", "Alice", "Bob");
// BEFORE Java 8: Anonymous inner class
Collections.sort(names, new Comparator() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
System.out.println("Sorted (anonymous class): " + names);
// Output: Sorted (anonymous class): [Alice, Bob, Charlie]
// AFTER Java 8: Lambda expression
List names2 = Arrays.asList("Charlie", "Alice", "Bob");
Collections.sort(names2, (a, b) -> a.compareTo(b));
System.out.println("Sorted (lambda): " + names2);
// Output: Sorted (lambda): [Alice, Bob, Charlie]
// EVEN SHORTER: Method reference
List names3 = Arrays.asList("Charlie", "Alice", "Bob");
names3.sort(String::compareTo);
System.out.println("Sorted (method reference): " + names3);
// Output: Sorted (method reference): [Alice, Bob, Charlie]
}
}
Five lines of anonymous class code reduced to a single expression. That is the power of lambdas.
The general syntax of a lambda expression is:
(parameters) -> expression
OR
(parameters) -> { statements; }
The arrow operator -> separates the parameter list from the body. The left side defines what goes in, the right side defines what comes out (or what happens).
Depending on the number of parameters and the complexity of the body, the syntax can be simplified in several ways:
| Variation | Syntax | Example |
|---|---|---|
| No parameters | () -> expression |
() -> System.out.println("Hello") |
| Single parameter (no parens needed) | param -> expression |
name -> name.toUpperCase() |
| Single parameter (with parens) | (param) -> expression |
(name) -> name.toUpperCase() |
| Multiple parameters | (p1, p2) -> expression |
(a, b) -> a + b |
| Expression body (implicit return) | (params) -> expression |
(x) -> x * x |
| Block body (explicit return) | (params) -> { return expr; } |
(x) -> { return x * x; } |
| Block body (void, no return) | (params) -> { statements; } |
(msg) -> { System.out.println(msg); } |
| Explicit parameter types | (Type p1, Type p2) -> expr |
(String a, String b) -> a.compareTo(b) |
import java.util.function.*;
public class LambdaSyntaxVariations {
public static void main(String[] args) {
// 1. No parameters
Runnable greet = () -> System.out.println("Hello, World!");
greet.run();
// Output: Hello, World!
// 2. Single parameter - parentheses optional
Consumer print = message -> System.out.println(message);
print.accept("Lambda with one param");
// Output: Lambda with one param
// 3. Single parameter - with parentheses (also valid)
Consumer print2 = (message) -> System.out.println(message);
print2.accept("Lambda with parens");
// Output: Lambda with parens
// 4. Multiple parameters
BinaryOperator add = (a, b) -> a + b;
System.out.println("Sum: " + add.apply(3, 7));
// Output: Sum: 10
// 5. Expression body - implicit return
Function square = x -> x * x;
System.out.println("Square of 5: " + square.apply(5));
// Output: Square of 5: 25
// 6. Block body - explicit return required
Function classify = x -> {
if (x > 0) {
return "Positive";
} else if (x < 0) {
return "Negative";
} else {
return "Zero";
}
};
System.out.println("10 is: " + classify.apply(10));
// Output: 10 is: Positive
// 7. Explicit types (usually unnecessary due to type inference)
BinaryOperator concat = (String a, String b) -> a + " " + b;
System.out.println(concat.apply("Hello", "Lambda"));
// Output: Hello Lambda
// 8. Multi-line block body with no return (void)
Consumer logger = (msg) -> {
String timestamp = java.time.LocalDateTime.now().toString();
System.out.println("[" + timestamp + "] " + msg);
};
logger.accept("Application started");
// Output: [2024-01-15T10:30:00.123] Application started
}
}
In most cases, the Java compiler can infer the parameter types from the context (the functional interface the lambda implements). You do not need to declare them explicitly.
The compiler looks at the target type — the functional interface type the lambda is being assigned to — and determines the parameter types from its single abstract method.
import java.util.Comparator;
import java.util.function.BiFunction;
public class TypeInference {
public static void main(String[] args) {
// The compiler knows this is Comparator, so a and b are String
Comparator comp1 = (a, b) -> a.compareTo(b);
// You CAN specify types explicitly -- sometimes useful for clarity
Comparator comp2 = (String a, String b) -> a.compareTo(b);
// IMPORTANT: You cannot mix -- either all types or no types
// Comparator comp3 = (String a, b) -> a.compareTo(b); // COMPILE ERROR
// Type inference works with generics too
BiFunction repeat = (text, times) -> text.repeat(times);
System.out.println(repeat.apply("Ha", 3));
// Output: HaHaHa
}
}
Lambdas do not exist in a vacuum. Every lambda expression in Java is an implementation of a functional interface. Understanding functional interfaces is essential to understanding lambdas.
A functional interface is an interface that has exactly one abstract method. It can have any number of default methods, static methods, and private methods — but only one abstract method. This single abstract method (SAM) is what the lambda implements.
Key rules:
default and static methodsObject (like toString(), equals()) do not count@FunctionalInterface annotation is optional but recommended — it causes a compile error if the interface has more than one abstract method// A functional interface - has exactly ONE abstract method
@FunctionalInterface
interface Greeting {
void greet(String name); // single abstract method
}
// Still a functional interface - default methods don't count
@FunctionalInterface
interface MathOperation {
double calculate(double a, double b); // single abstract method
default void printResult(double a, double b) {
System.out.println("Result: " + calculate(a, b));
}
}
// NOT a functional interface - has TWO abstract methods
// @FunctionalInterface // This would cause a compile error!
interface NotFunctional {
void methodOne();
void methodTwo();
}
// Still a functional interface - toString() comes from Object, doesn't count
@FunctionalInterface
interface Converter {
T convert(F from);
@Override
String toString(); // From Object -- does NOT count as abstract
}
You can create your own functional interfaces for domain-specific behavior. The @FunctionalInterface annotation tells the compiler (and other developers) that this interface is intended for lambda use.
@FunctionalInterface interface Validator{ boolean validate(T item); } @FunctionalInterface interface Transformer { R transform(T input); } @FunctionalInterface interface TriFunction { R apply(A a, B b, C c); } public class CustomFunctionalInterfaces { public static void main(String[] args) { // Using custom Validator Validator emailValidator = email -> email != null && email.contains("@") && email.contains("."); System.out.println("valid@email.com: " + emailValidator.validate("valid@email.com")); // Output: valid@email.com: true System.out.println("invalid: " + emailValidator.validate("invalid")); // Output: invalid: false // Using custom Transformer Transformer wordCounter = text -> text.split("\\s+").length; System.out.println("Word count: " + wordCounter.transform("Java lambdas are powerful")); // Output: Word count: 4 // Using custom TriFunction (Java doesn't provide one by default) TriFunction clamp = (value, min, max) -> Math.max(min, Math.min(max, value)); System.out.println("Clamp 15 to [0,10]: " + clamp.apply(15, 0, 10)); // Output: Clamp 15 to [0,10]: 10 System.out.println("Clamp 5 to [0,10]: " + clamp.apply(5, 0, 10)); // Output: Clamp 5 to [0,10]: 5 } }
Many interfaces that existed before Java 8 qualify as functional interfaces. The @FunctionalInterface annotation was added to them retroactively:
| Interface | Abstract Method | Package |
|---|---|---|
Runnable |
void run() |
java.lang |
Callable |
V call() |
java.util.concurrent |
Comparator |
int compare(T o1, T o2) |
java.util |
ActionListener |
void actionPerformed(ActionEvent e) |
java.awt.event |
This means you can use lambdas anywhere these interfaces are expected — no code changes needed on the caller side.
Java 8 introduced the java.util.function package with 43 functional interfaces. You do not need to memorize all of them. Most are specializations of four core interfaces. Master these four and the rest will follow naturally.
A Predicate takes one argument and returns a boolean. Use it for filtering, validation, and condition-checking.
| Method | Description |
|---|---|
boolean test(T t) |
The abstract method — evaluates the predicate on the given argument |
and(Predicate other) |
Logical AND — both predicates must be true |
or(Predicate other) |
Logical OR — at least one predicate must be true |
negate() |
Logical NOT — inverts the predicate |
Predicate.isEqual(target) |
Static method — creates predicate that tests equality to target |
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PredicateExamples {
public static void main(String[] args) {
// Basic predicate
Predicate isPositive = n -> n > 0;
System.out.println("5 is positive: " + isPositive.test(5)); // true
System.out.println("-3 is positive: " + isPositive.test(-3)); // false
// Composing predicates with and(), or(), negate()
Predicate isEven = n -> n % 2 == 0;
Predicate isPositiveAndEven = isPositive.and(isEven);
Predicate isPositiveOrEven = isPositive.or(isEven);
Predicate isNotPositive = isPositive.negate();
System.out.println("6 is positive AND even: " + isPositiveAndEven.test(6)); // true
System.out.println("3 is positive AND even: " + isPositiveAndEven.test(3)); // false
System.out.println("-4 is positive OR even: " + isPositiveOrEven.test(-4)); // true
System.out.println("-3 is NOT positive: " + isNotPositive.test(-3)); // true
// Practical example: filtering a list
List names = List.of("Alice", "Bob", "Charlie", "Dave", "Eve");
Predicate longerThan3 = name -> name.length() > 3;
Predicate startsWithC = name -> name.startsWith("C");
List filtered = names.stream()
.filter(longerThan3.and(startsWithC))
.collect(Collectors.toList());
System.out.println("Long names starting with C: " + filtered);
// Output: Long names starting with C: [Charlie]
// Predicate.isEqual() - useful for null-safe equality
Predicate isAlice = Predicate.isEqual("Alice");
System.out.println("Is Alice: " + isAlice.test("Alice")); // true
System.out.println("Is Alice: " + isAlice.test(null)); // false
}
}
A Function takes one argument of type T and returns a result of type R. Use it for transformations, conversions, and mappings.
| Method | Description |
|---|---|
R apply(T t) |
The abstract method — applies the function to the argument |
andThen(Function after) |
Compose: apply this function first, then apply after |
compose(Function before) |
Compose: apply before first, then apply this function |
Function.identity() |
Static method — returns a function that always returns its input |
import java.util.function.Function;
public class FunctionExamples {
public static void main(String[] args) {
// Basic function: String -> Integer
Function stringLength = s -> s.length();
System.out.println("Length of 'Lambda': " + stringLength.apply("Lambda"));
// Output: Length of 'Lambda': 6
// Function composition with andThen()
// Apply first function, then apply second to the result
Function toUpperCase = s -> s.toUpperCase();
Function addExclamation = s -> s + "!";
Function shout = toUpperCase.andThen(addExclamation);
System.out.println(shout.apply("hello"));
// Output: HELLO!
// Function composition with compose()
// Apply the argument function FIRST, then apply this function
Function multiplyBy2 = n -> n * 2;
Function add10 = n -> n + 10;
// compose: add10 runs first, then multiplyBy2
Function add10ThenDouble = multiplyBy2.compose(add10);
System.out.println("compose(5): " + add10ThenDouble.apply(5));
// Output: compose(5): 30 (5+10=15, 15*2=30)
// andThen: multiplyBy2 runs first, then add10
Function doubleThenAdd10 = multiplyBy2.andThen(add10);
System.out.println("andThen(5): " + doubleThenAdd10.apply(5));
// Output: andThen(5): 20 (5*2=10, 10+10=20)
// Function.identity() - returns input unchanged
Function identity = Function.identity();
System.out.println(identity.apply("unchanged"));
// Output: unchanged
// Practical: build a text processing pipeline
Function trim = String::trim;
Function lower = String::toLowerCase;
Function normalize = trim.andThen(lower).andThen(s -> s.replaceAll("\\s+", " "));
System.out.println("'" + normalize.apply(" Hello WORLD ") + "'");
// Output: 'hello world'
}
}
A Consumer takes one argument and returns nothing (void). Use it for actions, side effects, printing, logging, or saving data.
| Method | Description |
|---|---|
void accept(T t) |
The abstract method — performs the action on the argument |
andThen(Consumer after) |
Chain: perform this action, then perform after |
import java.util.List;
import java.util.function.Consumer;
public class ConsumerExamples {
public static void main(String[] args) {
// Basic consumer
Consumer print = s -> System.out.println(s);
print.accept("Hello from Consumer!");
// Output: Hello from Consumer!
// Chaining consumers with andThen()
Consumer toUpper = s -> System.out.println("Upper: " + s.toUpperCase());
Consumer toLower = s -> System.out.println("Lower: " + s.toLowerCase());
Consumer both = toUpper.andThen(toLower);
both.accept("Lambda");
// Output:
// Upper: LAMBDA
// Lower: lambda
// Practical: process a list of items
List emails = List.of("alice@example.com", "bob@example.com", "charlie@example.com");
Consumer validate = email -> {
if (!email.contains("@")) {
System.out.println("INVALID: " + email);
}
};
Consumer sendWelcome = email -> System.out.println("Sending welcome email to: " + email);
Consumer logAction = email -> System.out.println("Logged: processed " + email);
Consumer processEmail = validate.andThen(sendWelcome).andThen(logAction);
emails.forEach(processEmail);
// Output:
// Sending welcome email to: alice@example.com
// Logged: processed alice@example.com
// Sending welcome email to: bob@example.com
// Logged: processed bob@example.com
// Sending welcome email to: charlie@example.com
// Logged: processed charlie@example.com
}
}
A Supplier takes no arguments and returns a value. Use it for lazy evaluation, factory methods, and deferred computation.
| Method | Description |
|---|---|
T get() |
The abstract method — produces a result with no input |
import java.time.LocalDateTime;
import java.util.Random;
import java.util.function.Supplier;
public class SupplierExamples {
public static void main(String[] args) {
// Basic supplier
Supplier helloSupplier = () -> "Hello, World!";
System.out.println(helloSupplier.get());
// Output: Hello, World!
// Supplier for current timestamp
Supplier now = () -> LocalDateTime.now();
System.out.println("Current time: " + now.get());
// Output: Current time: 2024-01-15T10:30:00.123
// Supplier as a factory
Supplier randomFactory = () -> new Random();
Random r1 = randomFactory.get();
Random r2 = randomFactory.get();
System.out.println("Same instance? " + (r1 == r2)); // false -- new object each time
// Lazy evaluation -- the expensive computation only runs when needed
Supplier expensiveCalculation = () -> {
System.out.println(" ...performing expensive calculation...");
double result = 0;
for (int i = 0; i < 1000; i++) {
result += Math.sqrt(i);
}
return result;
};
boolean needResult = true;
if (needResult) {
System.out.println("Result: " + expensiveCalculation.get());
}
// Output:
// ...performing expensive calculation...
// Result: 21065.833...
// Supplier for default values
String name = null;
Supplier defaultName = () -> "Anonymous";
String displayName = (name != null) ? name : defaultName.get();
System.out.println("Name: " + displayName);
// Output: Name: Anonymous
}
}
UnaryOperator is a specialization of Function where the input and output types are the same. BinaryOperator is a specialization of BiFunction. These are convenience interfaces for operations that do not change the type.
import java.util.Arrays;
import java.util.List;
import java.util.function.BinaryOperator;
import java.util.function.UnaryOperator;
public class OperatorExamples {
public static void main(String[] args) {
// UnaryOperator: same input and output type
UnaryOperator toUpper = s -> s.toUpperCase();
System.out.println(toUpper.apply("lambda"));
// Output: LAMBDA
UnaryOperator doubleIt = n -> n * 2;
System.out.println(doubleIt.apply(7));
// Output: 14
// UnaryOperator with List.replaceAll()
List names = Arrays.asList("alice", "bob", "charlie");
names.replaceAll(String::toUpperCase);
System.out.println(names);
// Output: [ALICE, BOB, CHARLIE]
// BinaryOperator: two inputs of same type, same output type
BinaryOperator max = (a, b) -> a > b ? a : b;
System.out.println("Max of 5 and 9: " + max.apply(5, 9));
// Output: Max of 5 and 9: 9
BinaryOperator join = (a, b) -> a + ", " + b;
System.out.println(join.apply("Hello", "World"));
// Output: Hello, World
// BinaryOperator with reduce()
List numbers = List.of(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, Integer::sum);
System.out.println("Sum: " + sum);
// Output: Sum: 15
// BinaryOperator.minBy() and maxBy()
BinaryOperator longerString = BinaryOperator.maxBy(
(a, b) -> Integer.compare(a.length(), b.length())
);
System.out.println(longerString.apply("short", "much longer"));
// Output: much longer
}
}
Java provides “Bi” versions of Function, Predicate, and Consumer that accept two arguments instead of one.
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
public class BiFunctionExamples {
public static void main(String[] args) {
// BiFunction - takes two args, returns a result
BiFunction repeat = (text, times) -> text.repeat(times);
System.out.println(repeat.apply("Ha", 3));
// Output: HaHaHa
// BiPredicate - takes two args, returns boolean
BiPredicate isLongerThan = (str, length) -> str.length() > length;
System.out.println("'Lambda' longer than 3? " + isLongerThan.test("Lambda", 3));
// Output: 'Lambda' longer than 3? true
System.out.println("'Hi' longer than 3? " + isLongerThan.test("Hi", 3));
// Output: 'Hi' longer than 3? false
// BiConsumer - takes two args, returns nothing
BiConsumer printEntry = (key, value) ->
System.out.println(key + " = " + value);
// BiConsumer is especially useful with Map.forEach()
Map scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 87);
scores.put("Charlie", 92);
System.out.println("Scores:");
scores.forEach(printEntry);
// Output:
// Scores:
// Alice = 95
// Bob = 87
// Charlie = 92
// BiFunction with Map.replaceAll()
Map prices = new HashMap<>();
prices.put("Apple", 100);
prices.put("Banana", 50);
prices.put("Cherry", 200);
// Apply 10% discount to everything
prices.replaceAll((item, price) -> (int)(price * 0.9));
System.out.println("Discounted: " + prices);
// Output: Discounted: {Apple=90, Banana=45, Cherry=180}
}
}
Here is a summary of the most commonly used functional interfaces from java.util.function:
| Interface | Abstract Method | Input | Output | Use Case |
|---|---|---|---|---|
Predicate |
test(T) |
T | boolean | Filtering, validation |
BiPredicate |
test(T, U) |
T, U | boolean | Two-argument conditions |
Function |
apply(T) |
T | R | Transformation, mapping |
BiFunction |
apply(T, U) |
T, U | R | Two-argument transformation |
Consumer |
accept(T) |
T | void | Printing, logging, saving |
BiConsumer |
accept(T, U) |
T, U | void | Map.forEach(), two-arg actions |
Supplier |
get() |
none | T | Factories, lazy evaluation |
UnaryOperator |
apply(T) |
T | T | Same-type transformation |
BinaryOperator |
apply(T, T) |
T, T | T | Reduction, combining |
There are also primitive specializations like IntPredicate, LongFunction, DoubleSupplier, IntUnaryOperator, and others that avoid autoboxing overhead. Use them when working with primitive types in performance-sensitive code.
Java 8 added several methods to the Collection interfaces that accept functional interfaces — making lambdas a natural fit for everyday collection operations. These methods let you process data in place without creating streams.
import java.util.*;
public class LambdaWithCollections {
public static void main(String[] args) {
// ========== forEach() ==========
// Iterable.forEach(Consumer) - perform an action on each element
List fruits = Arrays.asList("Apple", "Banana", "Cherry", "Date");
System.out.println("--- forEach ---");
fruits.forEach(fruit -> System.out.println("Fruit: " + fruit));
// Output:
// Fruit: Apple
// Fruit: Banana
// Fruit: Cherry
// Fruit: Date
// forEach on a Map
Map ages = new LinkedHashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
ages.put("Charlie", 35);
System.out.println("\n--- Map forEach ---");
ages.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));
// Output:
// Alice is 30 years old
// Bob is 25 years old
// Charlie is 35 years old
// ========== removeIf() ==========
// Collection.removeIf(Predicate) - remove elements that match condition
List numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
numbers.removeIf(n -> n % 2 == 0); // Remove all even numbers
System.out.println("\n--- removeIf (removed evens) ---");
System.out.println(numbers);
// Output: [1, 3, 5, 7, 9]
// ========== replaceAll() ==========
// List.replaceAll(UnaryOperator) - transform each element in place
List names = new ArrayList<>(Arrays.asList("alice", "bob", "charlie"));
names.replaceAll(name -> name.substring(0, 1).toUpperCase() + name.substring(1));
System.out.println("\n--- replaceAll (capitalized) ---");
System.out.println(names);
// Output: [Alice, Bob, Charlie]
// ========== sort() ==========
// List.sort(Comparator) - sort the list using a lambda comparator
List cities = new ArrayList<>(Arrays.asList("New York", "London", "Tokyo", "Paris", "Sydney"));
// Sort alphabetically
cities.sort((a, b) -> a.compareTo(b));
System.out.println("\n--- sort (alphabetical) ---");
System.out.println(cities);
// Output: [London, New York, Paris, Sydney, Tokyo]
// Sort by length
cities.sort((a, b) -> Integer.compare(a.length(), b.length()));
System.out.println("\n--- sort (by length) ---");
System.out.println(cities);
// Output: [Paris, Tokyo, London, Sydney, New York]
// Using Comparator helper methods (cleaner than raw lambda)
cities.sort(Comparator.comparingInt(String::length).reversed());
System.out.println("\n--- sort (by length, descending) ---");
System.out.println(cities);
// Output: [New York, London, Sydney, Paris, Tokyo]
// ========== Map.computeIfAbsent() ==========
// Compute a value only if the key is not already present
Map> groups = new HashMap<>();
groups.computeIfAbsent("fruits", k -> new ArrayList<>()).add("Apple");
groups.computeIfAbsent("fruits", k -> new ArrayList<>()).add("Banana");
groups.computeIfAbsent("veggies", k -> new ArrayList<>()).add("Carrot");
System.out.println("\n--- computeIfAbsent ---");
System.out.println(groups);
// Output: {veggies=[Carrot], fruits=[Apple, Banana]}
// ========== Map.merge() ==========
// Merge a new value with an existing value
Map wordCount = new HashMap<>();
String[] words = {"apple", "banana", "apple", "cherry", "banana", "apple"};
for (String word : words) {
wordCount.merge(word, 1, (oldVal, newVal) -> oldVal + newVal);
}
System.out.println("\n--- merge (word count) ---");
System.out.println(wordCount);
// Output: {banana=2, cherry=1, apple=3}
}
}
The Stream API is where lambdas truly shine. Streams provide a declarative pipeline for processing collections, and virtually every stream operation accepts a lambda expression. Here are the most common operations showing lambda syntax alongside method reference alternatives.
import java.util.*;
import java.util.stream.Collectors;
public class LambdaWithStreams {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie", "David", "Eve", "Alice");
// ========== filter() -- takes a Predicate ==========
// Lambda version
List longNames = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
System.out.println("Filter (lambda): " + longNames);
// Output: Filter (lambda): [Alice, Charlie, David, Alice]
// ========== map() -- takes a Function ==========
// Lambda version
List nameLengths = names.stream()
.map(name -> name.length())
.collect(Collectors.toList());
System.out.println("Map (lambda): " + nameLengths);
// Output: Map (lambda): [5, 3, 7, 5, 3, 5]
// Method reference version
List upperNames = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println("Map (method ref): " + upperNames);
// Output: Map (method ref): [ALICE, BOB, CHARLIE, DAVID, EVE, ALICE]
// ========== reduce() -- takes a BinaryOperator ==========
List numbers = List.of(1, 2, 3, 4, 5);
// Lambda version
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("Reduce (lambda): " + sum);
// Output: Reduce (lambda): 15
// Method reference version
int sum2 = numbers.stream()
.reduce(0, Integer::sum);
System.out.println("Reduce (method ref): " + sum2);
// Output: Reduce (method ref): 15
// ========== collect() -- grouping with lambdas ==========
List allNames = List.of("Alice", "Anna", "Bob", "Bill", "Charlie", "Chris");
Map> grouped = allNames.stream()
.collect(Collectors.groupingBy(name -> name.charAt(0)));
System.out.println("Grouped: " + grouped);
// Output: Grouped: {A=[Alice, Anna], B=[Bob, Bill], C=[Charlie, Chris]}
// ========== sorted() -- takes a Comparator ==========
List sorted = allNames.stream()
.sorted((a, b) -> Integer.compare(a.length(), b.length()))
.collect(Collectors.toList());
System.out.println("Sorted by length: " + sorted);
// Output: Sorted by length: [Bob, Bill, Anna, Chris, Alice, Charlie]
// Comparator helper (cleaner)
List sorted2 = allNames.stream()
.sorted(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()))
.collect(Collectors.toList());
System.out.println("Sorted by length then alpha: " + sorted2);
// Output: Sorted by length then alpha: [Bob, Anna, Bill, Alice, Chris, Charlie]
// ========== forEach() -- takes a Consumer ==========
System.out.println("forEach:");
names.stream()
.distinct()
.forEach(name -> System.out.println(" - " + name));
// Output:
// forEach:
// - Alice
// - Bob
// - Charlie
// - David
// - Eve
// ========== Combining multiple operations ==========
String result = names.stream()
.filter(name -> name.length() > 3) // Predicate
.map(String::toUpperCase) // Function (method ref)
.distinct() // Remove duplicates
.sorted() // Natural order
.collect(Collectors.joining(", ")); // Join into a string
System.out.println("Pipeline: " + result);
// Output: Pipeline: ALICE, CHARLIE, DAVID
}
}
A lambda expression can access variables from its enclosing scope — this is called variable capture. However, there are strict rules about which variables can be accessed and how.
A lambda can access a local variable from its enclosing scope only if that variable is effectively final — meaning its value is never modified after initialization. You do not need to explicitly declare it final, but you cannot change it.
import java.util.List;
import java.util.function.Consumer;
public class VariableCapture {
// Instance variable - CAN be modified in lambdas
private int instanceCounter = 0;
// Static variable - CAN be modified in lambdas
private static int staticCounter = 0;
public void demonstrate() {
// ===== Local variables must be effectively final =====
// This works -- prefix is effectively final (never reassigned)
String prefix = "Hello";
Consumer greeter = name -> System.out.println(prefix + ", " + name);
greeter.accept("Alice");
// Output: Hello, Alice
// This DOES NOT compile -- count is modified after the lambda captures it
// int count = 0;
// Runnable r = () -> System.out.println(count); // OK so far
// count = 1; // ERROR: Variable used in lambda must be effectively final
// This DOES NOT compile either -- you cannot modify a captured variable inside a lambda
// int total = 0;
// List.of(1, 2, 3).forEach(n -> total += n); // ERROR: Cannot modify local variable
// ===== Instance variables CAN be modified =====
List.of(1, 2, 3).forEach(n -> instanceCounter += n);
System.out.println("Instance counter: " + instanceCounter);
// Output: Instance counter: 6
// ===== Static variables CAN be modified =====
List.of(1, 2, 3).forEach(n -> staticCounter += n);
System.out.println("Static counter: " + staticCounter);
// Output: Static counter: 6
}
public static void main(String[] args) {
new VariableCapture().demonstrate();
}
}
The restriction exists because lambdas capture a copy of local variables, not a reference to them. Local variables live on the stack and disappear when the method returns, but the lambda might be executed later (e.g., in another thread). If the lambda modified its copy, changes would not reflect in the original — creating confusing bugs. Java prevents this at compile time.
Instance and static variables are different — they live on the heap and are accessed through references, so lambdas can read and modify them safely.
When you genuinely need to accumulate or modify a value inside a lambda, use one of these approaches:
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class VariableCaptureWorkarounds {
public static void main(String[] args) {
List numbers = List.of(1, 2, 3, 4, 5);
// Workaround 1: AtomicInteger (preferred for thread-safe counting)
AtomicInteger atomicSum = new AtomicInteger(0);
numbers.forEach(n -> atomicSum.addAndGet(n));
System.out.println("AtomicInteger sum: " + atomicSum.get());
// Output: AtomicInteger sum: 15
// Workaround 2: Single-element array (the array reference is effectively final)
int[] arraySum = {0};
numbers.forEach(n -> arraySum[0] += n);
System.out.println("Array wrapper sum: " + arraySum[0]);
// Output: Array wrapper sum: 15
// Workaround 3: Use stream reduce() instead (BEST approach -- no side effects)
int streamSum = numbers.stream().reduce(0, Integer::sum);
System.out.println("Stream reduce sum: " + streamSum);
// Output: Stream reduce sum: 15
// Workaround 4: Mutable container
List results = new java.util.ArrayList<>();
numbers.forEach(n -> {
if (n % 2 == 0) {
results.add("Even: " + n);
}
});
System.out.println("Results: " + results);
// Output: Results: [Even: 2, Even: 4]
// BEST PRACTICE: Prefer stream operations over mutation
List betterResults = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> "Even: " + n)
.collect(java.util.stream.Collectors.toList());
System.out.println("Better results: " + betterResults);
// Output: Better results: [Even: 2, Even: 4]
}
}
Before lambdas, anonymous inner classes were the primary way to pass behavior as an argument. Both achieve similar goals, but they differ in important ways.
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class LambdaVsAnonymousClass {
private String instanceField = "I'm an instance field";
public void compare() {
List names = Arrays.asList("Charlie", "Alice", "Bob");
// ========== Anonymous inner class ==========
names.sort(new Comparator() {
@Override
public int compare(String a, String b) {
// 'this' refers to the anonymous Comparator instance
System.out.println("this class: " + this.getClass().getSimpleName());
return a.compareTo(b);
}
});
System.out.println("Anonymous class sort: " + names);
// ========== Lambda expression ==========
List names2 = Arrays.asList("Charlie", "Alice", "Bob");
names2.sort((a, b) -> {
// 'this' refers to the enclosing LambdaVsAnonymousClass instance
System.out.println("this field: " + this.instanceField);
return a.compareTo(b);
});
System.out.println("Lambda sort: " + names2);
}
public static void main(String[] args) {
new LambdaVsAnonymousClass().compare();
// Output:
// this class:
// this class:
// Anonymous class sort: [Alice, Bob, Charlie]
// this field: I'm an instance field
// this field: I'm an instance field
// Lambda sort: [Alice, Bob, Charlie]
}
}
| Aspect | Anonymous Class | Lambda Expression |
|---|---|---|
| Syntax | Verbose — requires new Interface() { ... } |
Concise — (params) -> body |
this keyword |
Refers to the anonymous class instance | Refers to the enclosing class instance |
| Interface requirement | Can implement any interface (including multi-method) | Can only implement a functional interface (single abstract method) |
| State | Can have its own fields and state | Cannot have fields — stateless |
| Compilation | Generates a separate .class file (e.g., Outer$1.class) |
Uses invokedynamic — no extra class file |
| Performance | Slightly more overhead (class loading) | Slightly better (deferred binding with invokedynamic) |
| Readability | Harder to read for simple operations | Much cleaner for simple operations |
| Shadowing | Can shadow variables from enclosing scope | Cannot shadow — shares enclosing scope |
Use a lambda when:
this to refer to the implementation itselfUse an anonymous class when:
this to refer to the implementation instanceimport java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class MigrationExample {
public static void main(String[] args) {
List names = new ArrayList<>(List.of("Charlie", "Alice", "Bob", "David"));
// STEP 1: Original anonymous class
Collections.sort(names, new java.util.Comparator() {
@Override
public int compare(String a, String b) {
return a.compareToIgnoreCase(b);
}
});
System.out.println("Step 1 (anonymous): " + names);
// STEP 2: Replace with lambda
names = new ArrayList<>(List.of("Charlie", "Alice", "Bob", "David"));
Collections.sort(names, (a, b) -> a.compareToIgnoreCase(b));
System.out.println("Step 2 (lambda): " + names);
// STEP 3: Use List.sort() instead of Collections.sort()
names = new ArrayList<>(List.of("Charlie", "Alice", "Bob", "David"));
names.sort((a, b) -> a.compareToIgnoreCase(b));
System.out.println("Step 3 (List.sort): " + names);
// STEP 4: Use method reference
names = new ArrayList<>(List.of("Charlie", "Alice", "Bob", "David"));
names.sort(String::compareToIgnoreCase);
System.out.println("Step 4 (method ref): " + names);
// All output: [Alice, Bob, Charlie, David]
}
}
A method reference is a shorthand notation for a lambda expression that simply calls an existing method. If your lambda does nothing more than call a single method, a method reference is cleaner.
There are four types of method references:
| Type | Syntax | Lambda Equivalent | Example |
|---|---|---|---|
| Static method | Class::staticMethod |
(args) -> Class.staticMethod(args) |
Integer::parseInt |
| Instance method (bound) | object::instanceMethod |
(args) -> object.instanceMethod(args) |
System.out::println |
| Instance method (unbound) | Class::instanceMethod |
(obj, args) -> obj.instanceMethod(args) |
String::toUpperCase |
| Constructor | Class::new |
(args) -> new Class(args) |
ArrayList::new |
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class MethodReferenceExamples {
public static void main(String[] args) {
List words = List.of("hello", "world", "java", "lambda");
// ========== 1. Static method reference ==========
// Lambda: s -> Integer.parseInt(s)
// Method ref: Integer::parseInt
List numberStrings = List.of("1", "2", "3", "4", "5");
List numbers = numberStrings.stream()
.map(Integer::parseInt) // static method reference
.collect(Collectors.toList());
System.out.println("Static: " + numbers);
// Output: Static: [1, 2, 3, 4, 5]
// ========== 2. Bound instance method reference ==========
// Lambda: s -> System.out.println(s)
// Method ref: System.out::println
System.out.println("Bound instance:");
words.forEach(System.out::println); // bound to System.out
// Output:
// hello
// world
// java
// lambda
// ========== 3. Unbound instance method reference ==========
// Lambda: s -> s.toUpperCase()
// Method ref: String::toUpperCase
List upper = words.stream()
.map(String::toUpperCase) // unbound -- called on each element
.collect(Collectors.toList());
System.out.println("Unbound: " + upper);
// Output: Unbound: [HELLO, WORLD, JAVA, LAMBDA]
// Unbound with two arguments (used in Comparator)
// Lambda: (a, b) -> a.compareToIgnoreCase(b)
// Method ref: String::compareToIgnoreCase
List sorted = Arrays.asList("banana", "Apple", "cherry");
sorted.sort(String::compareToIgnoreCase);
System.out.println("Sorted: " + sorted);
// Output: Sorted: [Apple, banana, cherry]
// ========== 4. Constructor reference ==========
// Lambda: () -> new ArrayList()
// Method ref: ArrayList::new
Supplier> listFactory = java.util.ArrayList::new;
List newList = listFactory.get();
newList.add("Created with constructor reference");
System.out.println("Constructor: " + newList);
// Output: Constructor: [Created with constructor reference]
// Constructor reference with parameters
Function sbFactory = StringBuilder::new;
StringBuilder sb = sbFactory.apply("Initial value");
System.out.println("StringBuilder: " + sb);
// Output: StringBuilder: Initial value
}
}
Rule of thumb: If your lambda is (x) -> someMethod(x) or (x) -> x.someMethod(), it can usually be replaced with a method reference. Use method references when they improve clarity; stick with lambdas when the reference would be confusing.
Lambdas are not just syntactic sugar — they enable cleaner implementations of well-known design patterns. Here are patterns you will use regularly.
Lambdas simplify callback-style programming. Instead of creating a class for every callback, pass behavior directly.
import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; // A simple event system using lambdas as callbacks class EventEmitter{ private final List > listeners = new ArrayList<>(); public void on(Consumer listener) { listeners.add(listener); } public void emit(T event) { listeners.forEach(listener -> listener.accept(event)); } } public class EventHandlingPattern { public static void main(String[] args) { EventEmitter emitter = new EventEmitter<>(); // Register listeners using lambdas emitter.on(msg -> System.out.println("[LOG] " + msg)); emitter.on(msg -> System.out.println("[ALERT] " + msg.toUpperCase())); emitter.on(msg -> { if (msg.contains("error")) { System.out.println("[ERROR HANDLER] Escalating: " + msg); } }); emitter.emit("User logged in"); // Output: // [LOG] User logged in // [ALERT] USER LOGGED IN System.out.println(); emitter.emit("Database connection error"); // Output: // [LOG] Database connection error // [ALERT] DATABASE CONNECTION ERROR // [ERROR HANDLER] Escalating: Database connection error } }
The Strategy pattern defines a family of algorithms and makes them interchangeable. With lambdas, you no longer need a separate class for each strategy.
import java.util.function.BiFunction;
public class StrategyPattern {
// Before lambdas: separate classes for each strategy
interface DiscountStrategy {
double applyDiscount(double price, int quantity);
}
// With lambdas: strategies are just functions
public static void main(String[] args) {
// Define strategies as lambdas
BiFunction noDiscount =
(price, qty) -> price * qty;
BiFunction percentageDiscount =
(price, qty) -> price * qty * 0.9; // 10% off
BiFunction bulkDiscount =
(price, qty) -> qty >= 10 ? price * qty * 0.8 : price * qty; // 20% off for 10+
BiFunction buyOneGetOneFree =
(price, qty) -> price * (qty - qty / 2); // Every second item free
// Use the strategies
double price = 25.0;
System.out.println("No discount (5 items): $" + noDiscount.apply(price, 5));
// Output: No discount (5 items): $125.0
System.out.println("10% off (5 items): $" + percentageDiscount.apply(price, 5));
// Output: 10% off (5 items): $112.5
System.out.println("Bulk (15 items): $" + bulkDiscount.apply(price, 15));
// Output: Bulk (15 items): $300.0
System.out.println("BOGO (6 items): $" + buyOneGetOneFree.apply(price, 6));
// Output: BOGO (6 items): $75.0
}
}
The Decorator pattern wraps behavior around a function. With lambdas, you compose decorators by chaining Function instances.
import java.util.function.Function;
public class DecoratorPattern {
// A decorator that adds logging around any function
static Function withLogging(String name, Function fn) {
return input -> {
System.out.println(" [LOG] Calling " + name + " with: " + input);
R result = fn.apply(input);
System.out.println(" [LOG] " + name + " returned: " + result);
return result;
};
}
// A decorator that adds timing around any function
static Function withTiming(String name, Function fn) {
return input -> {
long start = System.nanoTime();
R result = fn.apply(input);
long elapsed = System.nanoTime() - start;
System.out.println(" [TIMING] " + name + " took " + elapsed / 1000 + " microseconds");
return result;
};
}
public static void main(String[] args) {
// Original function
Function reverseString = s ->
new StringBuilder(s).reverse().toString();
// Decorate with logging
Function loggedReverse = withLogging("reverse", reverseString);
// Decorate with logging AND timing
Function fullReverse = withTiming("reverse", withLogging("reverse", reverseString));
System.out.println("--- Logged only ---");
String result = loggedReverse.apply("Lambda");
System.out.println("Result: " + result);
// Output:
// [LOG] Calling reverse with: Lambda
// [LOG] reverse returned: adbmaL
// Result: adbmaL
System.out.println("\n--- Logged and timed ---");
result = fullReverse.apply("Decorator");
System.out.println("Result: " + result);
// Output:
// [TIMING] reverse took ... microseconds
// [LOG] Calling reverse with: Decorator
// [LOG] reverse returned: rotaroceD
// Result: rotaroceD
}
}
Lambdas enable lazy evaluation — deferring computation until the result is actually needed. This can save significant resources when a value might not be used.
import java.util.function.Supplier;
public class LazyEvaluation {
// Simulates an expensive computation
static String loadConfiguration() {
System.out.println(" Loading configuration from disk...");
try { Thread.sleep(100); } catch (InterruptedException e) {}
return "DB_URL=jdbc:mysql://localhost:3306/mydb";
}
// Without lazy evaluation: always computes the value
static void logEager(boolean isDebug, String message) {
if (isDebug) {
System.out.println("[DEBUG] " + message);
}
}
// With lazy evaluation: computes only if needed
static void logLazy(boolean isDebug, Supplier messageSupplier) {
if (isDebug) {
System.out.println("[DEBUG] " + messageSupplier.get());
}
}
public static void main(String[] args) {
boolean debugMode = false;
// EAGER: loadConfiguration() runs even though debugMode is false
System.out.println("--- Eager (debug=false) ---");
logEager(debugMode, "Config: " + loadConfiguration());
// Output:
// Loading configuration from disk...
// (the value was computed but never used!)
// LAZY: loadConfiguration() does NOT run because debugMode is false
System.out.println("\n--- Lazy (debug=false) ---");
logLazy(debugMode, () -> "Config: " + loadConfiguration());
// Output: (nothing -- the supplier was never called)
// LAZY with debug enabled
debugMode = true;
System.out.println("\n--- Lazy (debug=true) ---");
logLazy(debugMode, () -> "Config: " + loadConfiguration());
// Output:
// Loading configuration from disk...
// [DEBUG] Config: DB_URL=jdbc:mysql://localhost:3306/mydb
}
}
Even experienced developers make mistakes with lambdas. Here are the most common pitfalls and how to avoid them.
The built-in functional interfaces (Function, Consumer, Predicate, etc.) do not declare checked exceptions. If your lambda needs to throw a checked exception, it will not compile.
import java.util.List;
import java.util.function.Function;
public class CheckedExceptionMistake {
// This is a method that throws a checked exception
static String readFile(String path) throws java.io.IOException {
// Simulate reading a file
if (path.contains("missing")) {
throw new java.io.IOException("File not found: " + path);
}
return "Content of " + path;
}
// Custom functional interface that allows checked exceptions
@FunctionalInterface
interface ThrowingFunction {
R apply(T t) throws Exception;
}
// Wrapper method to convert a throwing function into a standard Function
static Function unchecked(ThrowingFunction fn) {
return t -> {
try {
return fn.apply(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
public static void main(String[] args) {
List paths = List.of("file1.txt", "file2.txt");
// PROBLEM: This does NOT compile!
// paths.stream()
// .map(path -> readFile(path)) // ERROR: Unhandled IOException
// .forEach(System.out::println);
// SOLUTION 1: Wrap in try-catch inside the lambda
paths.stream()
.map(path -> {
try {
return readFile(path);
} catch (java.io.IOException e) {
throw new RuntimeException(e);
}
})
.forEach(System.out::println);
// Output:
// Content of file1.txt
// Content of file2.txt
// SOLUTION 2: Use a wrapper function (cleaner)
paths.stream()
.map(unchecked(CheckedExceptionMistake::readFile))
.forEach(System.out::println);
// Output:
// Content of file1.txt
// Content of file2.txt
}
}
Lambdas used in stream operations should be side-effect-free. Modifying external state from inside a stream pipeline leads to unpredictable behavior, especially with parallel streams.
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class SideEffectMistake {
public static void main(String[] args) {
List names = List.of("Alice", "Bob", "Charlie", "David");
// BAD: Modifying external list from inside map()
List results = new ArrayList<>();
names.stream()
.map(String::toUpperCase)
.forEach(name -> results.add(name)); // side effect!
System.out.println("Bad (side effect): " + results);
// This might work with sequential streams, but BREAKS with parallel streams
// GOOD: Use collect() to build the result
List betterResults = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println("Good (collect): " + betterResults);
// Output: Good (collect): [ALICE, BOB, CHARLIE, DAVID]
// BAD: Accumulating a count with side effects
int[] count = {0};
names.stream().forEach(n -> count[0]++);
System.out.println("Bad count: " + count[0]); // works but fragile
// GOOD: Use count()
long goodCount = names.stream().count();
System.out.println("Good count: " + goodCount);
// Output: Good count: 4
}
}
If a lambda spans more than 3-4 lines, it is too complex. Extract it into a named method for readability, testability, and reuse.
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class ComplexLambdaMistake {
// BAD: This lambda is too complex
static List filterBad(List emails) {
return emails.stream()
.filter(email -> {
if (email == null || email.isBlank()) return false;
if (!email.contains("@")) return false;
String[] parts = email.split("@");
if (parts.length != 2) return false;
String domain = parts[1];
if (!domain.contains(".")) return false;
if (domain.startsWith(".") || domain.endsWith(".")) return false;
return true;
})
.collect(Collectors.toList());
}
// GOOD: Extract the logic into a named method
static boolean isValidEmail(String email) {
if (email == null || email.isBlank()) return false;
if (!email.contains("@")) return false;
String[] parts = email.split("@");
if (parts.length != 2) return false;
String domain = parts[1];
if (!domain.contains(".")) return false;
return !domain.startsWith(".") && !domain.endsWith(".");
}
static List filterGood(List emails) {
return emails.stream()
.filter(ComplexLambdaMistake::isValidEmail) // Clean and readable
.collect(Collectors.toList());
}
public static void main(String[] args) {
List emails = List.of(
"alice@example.com",
"invalid",
"",
"bob@test.org",
"bad@.com",
"ok@domain.io"
);
System.out.println("Valid emails: " + filterGood(emails));
// Output: Valid emails: [alice@example.com, bob@test.org, ok@domain.io]
}
}
Lambdas can only be used where a functional interface is expected. You cannot use a lambda to implement an interface with multiple abstract methods, or assign a lambda to an Object variable without a cast.
public class FunctionalInterfaceRequirement {
// Interface with TWO abstract methods -- NOT functional
interface TwoMethods {
void methodA();
void methodB();
}
public static void main(String[] args) {
// ERROR: Cannot use lambda -- TwoMethods is not a functional interface
// TwoMethods t = () -> System.out.println("Hello"); // COMPILE ERROR
// ERROR: Cannot assign lambda to Object without cast
// Object obj = () -> System.out.println("Hello"); // COMPILE ERROR
// FIX: Cast to a specific functional interface
Object obj = (Runnable) () -> System.out.println("Hello");
((Runnable) obj).run();
// Output: Hello
// COMMON GOTCHA: Overloaded methods can cause ambiguity
// If a method accepts both Runnable and Callable, the compiler might not
// know which one a no-arg lambda should map to.
}
}
Lambdas are not serializable by default. If you need to serialize a lambda (e.g., for distributed computing frameworks), the target functional interface must extend Serializable.
import java.io.*;
import java.util.function.Predicate;
public class SerializationMistake {
// Regular functional interface -- NOT serializable
@FunctionalInterface
interface RegularPredicate {
boolean test(T t);
}
// Serializable functional interface
@FunctionalInterface
interface SerializablePredicate extends Predicate, Serializable {
}
public static void main(String[] args) {
// This lambda is NOT serializable
RegularPredicate notSerializable = s -> s.length() > 5;
// This lambda IS serializable
SerializablePredicate serializable = s -> s.length() > 5;
// Or use an intersection cast (less clean but avoids a custom interface)
Predicate alsoSerializable = (Predicate & Serializable) s -> s.length() > 5;
System.out.println("Test 'Lambda': " + serializable.test("Lambda"));
// Output: Test 'Lambda': true
}
}
Follow these guidelines to write lambdas that are clean, maintainable, and efficient.
| # | Practice | Do | Don’t |
|---|---|---|---|
| 1 | Keep lambdas short | 1-3 lines max | Write 10+ line lambdas |
| 2 | Use method references | String::toUpperCase |
s -> s.toUpperCase() when a reference is clearer |
| 3 | Avoid side effects | collect() to build results |
Mutate external state in forEach() |
| 4 | Use meaningful parameter names | (name, age) -> ... |
(a, b) -> ... when context is unclear |
| 5 | Extract complex lambdas | Move to a named private method | Inline a 10-line validation lambda |
| 6 | Prefer standard interfaces | Use Predicate, Function, Consumer |
Create custom interface when a standard one fits |
| 7 | Use @FunctionalInterface | Annotate your custom interfaces | Rely on convention alone |
| 8 | Handle exceptions explicitly | Wrapper methods for checked exceptions | Swallow exceptions in catch blocks |
| 9 | Consider readability | Use anonymous class if lambda is confusing | Force everything into a lambda |
| 10 | Leverage type inference | (a, b) -> a + b |
(Integer a, Integer b) -> a + b when types are obvious |
import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;
public class LambdaBestPractices {
// BEST PRACTICE: Extract complex logic into named methods
static boolean isEligibleForDiscount(Map customer) {
int age = (int) customer.get("age");
boolean isMember = (boolean) customer.get("member");
double totalSpent = (double) customer.get("totalSpent");
return (age >= 65 || isMember) && totalSpent > 100.0;
}
// BEST PRACTICE: Use standard functional interfaces with clear names
static List filterBy(List items, Predicate criteria) {
return items.stream()
.filter(criteria)
.collect(Collectors.toList());
}
// BEST PRACTICE: Compose small, focused predicates
public static void main(String[] args) {
List words = List.of("Lambda", "is", "a", "powerful", "feature", "in", "Java");
// GOOD: Small, focused predicates composed together
Predicate longerThan2 = word -> word.length() > 2;
Predicate startsWithLower = word -> Character.isLowerCase(word.charAt(0));
List result = words.stream()
.filter(longerThan2.and(startsWithLower))
.map(String::toUpperCase) // method reference (cleaner)
.sorted() // natural order
.collect(Collectors.toList());
System.out.println("Filtered: " + result);
// Output: Filtered: [FEATURE, POWERFUL]
// GOOD: Meaningful parameter names
Map> grouped = words.stream()
.collect(Collectors.groupingBy(word -> word.substring(0, 1).toUpperCase()));
System.out.println("Grouped: " + grouped);
// GOOD: Use Comparator helpers instead of raw lambdas
List sortedByLength = new ArrayList<>(words);
sortedByLength.sort(
Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder())
);
System.out.println("Sorted: " + sortedByLength);
// Output: Sorted: [a, in, is, Java, Lambda, feature, powerful]
}
}
Let us put everything together with a real-world example. We will build a student records processing system that demonstrates lambdas for filtering, sorting, transforming, grouping, and reporting.
import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;
public class StudentDataProcessing {
// ========== Student record ==========
static class Student {
private final String name;
private final String major;
private final double gpa;
private final int age;
private final List courses;
Student(String name, String major, double gpa, int age, List courses) {
this.name = name;
this.major = major;
this.gpa = gpa;
this.age = age;
this.courses = courses;
}
public String getName() { return name; }
public String getMajor() { return major; }
public double getGpa() { return gpa; }
public int getAge() { return age; }
public List getCourses() { return courses; }
@Override
public String toString() {
return String.format("%s (Major: %s, GPA: %.1f, Age: %d)", name, major, gpa, age);
}
}
// ========== Custom functional interface for reporting ==========
@FunctionalInterface
interface ReportGenerator {
String generate(List data);
}
// ========== Utility: generic filter + transform pipeline ==========
static List pipeline(List data, Predicate filter, Function transform) {
return data.stream()
.filter(filter)
.map(transform)
.collect(Collectors.toList());
}
// ========== Main ==========
public static void main(String[] args) {
// Create sample data
List students = List.of(
new Student("Alice", "Computer Science", 3.8, 21, List.of("Java", "Algorithms", "Databases")),
new Student("Bob", "Mathematics", 3.2, 22, List.of("Calculus", "Statistics", "Algorithms")),
new Student("Charlie", "Computer Science", 3.5, 20, List.of("Java", "Networks", "AI")),
new Student("Diana", "Physics", 3.9, 23, List.of("Quantum", "Calculus", "Statistics")),
new Student("Eve", "Computer Science", 2.8, 21, List.of("Java", "Web Dev", "Databases")),
new Student("Frank", "Mathematics", 3.6, 22, List.of("Calculus", "Algorithms", "Statistics")),
new Student("Grace", "Physics", 3.1, 20, List.of("Quantum", "Mechanics", "Calculus")),
new Student("Hank", "Computer Science", 3.7, 23, List.of("Java", "AI", "Networks")),
new Student("Ivy", "Mathematics", 3.4, 21, List.of("Statistics", "Algebra", "Calculus")),
new Student("Jack", "Physics", 2.9, 22, List.of("Mechanics", "Quantum", "Statistics"))
);
System.out.println("=== STUDENT DATA PROCESSING SYSTEM ===\n");
// ===== 1. FILTERING with Predicate =====
System.out.println("--- 1. Honor Roll (GPA >= 3.5) ---");
Predicate isHonorRoll = student -> student.getGpa() >= 3.5;
students.stream()
.filter(isHonorRoll)
.forEach(s -> System.out.println(" " + s));
// Output:
// Alice (Major: Computer Science, GPA: 3.8, Age: 21)
// Charlie (Major: Computer Science, GPA: 3.5, Age: 20)
// Diana (Major: Physics, GPA: 3.9, Age: 23)
// Frank (Major: Mathematics, GPA: 3.6, Age: 22)
// Hank (Major: Computer Science, GPA: 3.7, Age: 23)
// ===== 2. COMPOSED PREDICATES =====
System.out.println("\n--- 2. CS students on Honor Roll ---");
Predicate isCS = s -> s.getMajor().equals("Computer Science");
Predicate csHonor = isCS.and(isHonorRoll);
students.stream()
.filter(csHonor)
.forEach(s -> System.out.println(" " + s));
// Output:
// Alice (Major: Computer Science, GPA: 3.8, Age: 21)
// Charlie (Major: Computer Science, GPA: 3.5, Age: 20)
// Hank (Major: Computer Science, GPA: 3.7, Age: 23)
// ===== 3. SORTING with Comparator lambdas =====
System.out.println("\n--- 3. All students sorted by GPA (descending) ---");
students.stream()
.sorted(Comparator.comparingDouble(Student::getGpa).reversed())
.forEach(s -> System.out.println(" " + s));
// Output:
// Diana (Major: Physics, GPA: 3.9, Age: 23)
// Alice (Major: Computer Science, GPA: 3.8, Age: 21)
// Hank (Major: Computer Science, GPA: 3.7, Age: 23)
// Frank (Major: Mathematics, GPA: 3.6, Age: 22)
// Charlie (Major: Computer Science, GPA: 3.5, Age: 20)
// Ivy (Major: Mathematics, GPA: 3.4, Age: 21)
// Bob (Major: Mathematics, GPA: 3.2, Age: 22)
// Grace (Major: Physics, GPA: 3.1, Age: 20)
// Jack (Major: Physics, GPA: 2.9, Age: 22)
// Eve (Major: Computer Science, GPA: 2.8, Age: 21)
// ===== 4. TRANSFORMATION with Function =====
System.out.println("\n--- 4. Student names in uppercase ---");
Function toNameUpper = s -> s.getName().toUpperCase();
List upperNames = students.stream()
.map(toNameUpper)
.collect(Collectors.toList());
System.out.println(" " + upperNames);
// Output: [ALICE, BOB, CHARLIE, DIANA, EVE, FRANK, GRACE, HANK, IVY, JACK]
// ===== 5. GROUPING with Collectors =====
System.out.println("\n--- 5. Students grouped by major ---");
Map> byMajor = students.stream()
.collect(Collectors.groupingBy(Student::getMajor));
byMajor.forEach((major, list) -> {
System.out.println(" " + major + ":");
list.forEach(s -> System.out.println(" - " + s.getName() + " (GPA: " + s.getGpa() + ")"));
});
// Output:
// Computer Science:
// - Alice (GPA: 3.8)
// - Charlie (GPA: 3.5)
// - Eve (GPA: 2.8)
// - Hank (GPA: 3.7)
// Mathematics:
// - Bob (GPA: 3.2)
// - Frank (GPA: 3.6)
// - Ivy (GPA: 3.4)
// Physics:
// - Diana (GPA: 3.9)
// - Grace (GPA: 3.1)
// - Jack (GPA: 2.9)
// ===== 6. STATISTICS with reduce and Collectors =====
System.out.println("\n--- 6. GPA Statistics by Major ---");
Map statsByMajor = students.stream()
.collect(Collectors.groupingBy(
Student::getMajor,
Collectors.summarizingDouble(Student::getGpa)
));
statsByMajor.forEach((major, stats) ->
System.out.printf(" %s: avg=%.2f, min=%.1f, max=%.1f%n",
major, stats.getAverage(), stats.getMin(), stats.getMax())
);
// Output:
// Computer Science: avg=3.45, min=2.8, max=3.8
// Mathematics: avg=3.40, min=3.2, max=3.6
// Physics: avg=3.30, min=2.9, max=3.9
// ===== 7. PIPELINE utility with Predicate + Function =====
System.out.println("\n--- 7. Pipeline: CS student names with high GPA ---");
List csHonorNames = pipeline(
students,
isCS.and(isHonorRoll), // composed Predicate
Student::getName // method reference as Function
);
System.out.println(" " + csHonorNames);
// Output: [Alice, Charlie, Hank]
// ===== 8. COURSE ANALYSIS with flatMap and lambdas =====
System.out.println("\n--- 8. Most popular courses ---");
Map courseCounts = students.stream()
.flatMap(s -> s.getCourses().stream())
.collect(Collectors.groupingBy(
course -> course, // grouping key
Collectors.counting() // count per group
));
courseCounts.entrySet().stream()
.sorted(Map.Entry.comparingByValue().reversed())
.forEach(entry -> System.out.println(" " + entry.getKey() + ": " + entry.getValue() + " students"));
// Output:
// Calculus: 4 students
// Java: 4 students
// Statistics: 4 students
// Quantum: 3 students
// Algorithms: 3 students
// ...
// ===== 9. CUSTOM REPORT with functional interface =====
System.out.println("\n--- 9. Custom Honor Roll Report ---");
ReportGenerator honorRollReport = data -> {
StringBuilder sb = new StringBuilder();
sb.append("Honor Roll Report\n");
sb.append("=================\n");
List honorStudents = data.stream()
.filter(isHonorRoll)
.sorted(Comparator.comparingDouble(Student::getGpa).reversed())
.collect(Collectors.toList());
sb.append(String.format("Total honor students: %d / %d%n", honorStudents.size(), data.size()));
sb.append(String.format("Percentage: %.0f%%%n%n",
(double) honorStudents.size() / data.size() * 100));
honorStudents.forEach(s ->
sb.append(String.format(" %-10s | %-20s | GPA: %.1f%n",
s.getName(), s.getMajor(), s.getGpa()))
);
return sb.toString();
};
System.out.println(honorRollReport.generate(students));
// ===== 10. CONSUMER chaining for notifications =====
System.out.println("--- 10. Student notifications ---");
Consumer emailNotification = s ->
System.out.println(" [EMAIL] Congratulations " + s.getName() + "! You made the honor roll.");
Consumer smsNotification = s ->
System.out.println(" [SMS] " + s.getName() + ", check your email for honor roll details.");
Consumer logNotification = s ->
System.out.println(" [LOG] Notification sent to " + s.getName());
Consumer notifyAll = emailNotification.andThen(smsNotification).andThen(logNotification);
students.stream()
.filter(csHonor)
.forEach(notifyAll);
// Output:
// [EMAIL] Congratulations Alice! You made the honor roll.
// [SMS] Alice, check your email for honor roll details.
// [LOG] Notification sent to Alice
// [EMAIL] Congratulations Charlie! You made the honor roll.
// [SMS] Charlie, check your email for honor roll details.
// [LOG] Notification sent to Charlie
// [EMAIL] Congratulations Hank! You made the honor roll.
// [SMS] Hank, check your email for honor roll details.
// [LOG] Notification sent to Hank
// ===== Summary =====
System.out.println("\n=== LAMBDA CONCEPTS DEMONSTRATED ===");
System.out.println("1. Predicate - filtering students by GPA");
System.out.println("2. Predicate.and() - combining CS + honor roll filters");
System.out.println("3. Comparator lambda - sorting by GPA descending");
System.out.println("4. Function - transforming student to name");
System.out.println("5. Collectors.groupingBy - grouping by major");
System.out.println("6. summarizingDouble - GPA statistics per major");
System.out.println("7. Pipeline utility - generic filter + transform method");
System.out.println("8. flatMap + lambda - course frequency analysis");
System.out.println("9. Custom @FunctionalInterface - report generation");
System.out.println("10. Consumer.andThen() - chained notification actions");
}
}
| Concept | Summary | Example |
|---|---|---|
| Lambda syntax | Parameters -> body | (a, b) -> a + b |
| Functional interface | Interface with one abstract method | @FunctionalInterface |
| Predicate | T -> boolean | n -> n > 0 |
| Function | T -> R | s -> s.length() |
| Consumer | T -> void | s -> System.out.println(s) |
| Supplier | () -> T | () -> new ArrayList<>() |
| UnaryOperator | T -> T | s -> s.toUpperCase() |
| BinaryOperator | (T, T) -> T | (a, b) -> a + b |
| Method reference | Shorthand for single-method lambda | String::toUpperCase |
| Effectively final | Local vars captured by lambdas cannot be modified | Use AtomicInteger or stream reduce() |
| this keyword | In lambdas, refers to enclosing class (not the lambda) | Unlike anonymous classes |
| Checked exceptions | Standard functional interfaces don’t allow checked exceptions | Use wrapper or custom interface |