1. What is a Lambda Expression?
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:
- Less boilerplate — Replace verbose anonymous classes with one-liners
- Readability — Code reads closer to what it does, not how it is wired up
- Functional programming — Pass behavior as arguments, return behavior from methods, store behavior in variables
- Foundation for Streams — The Stream API (filter, map, reduce) relies heavily on lambdas
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<String> names = Arrays.asList("Charlie", "Alice", "Bob");
// BEFORE Java 8: Anonymous inner class
Collections.sort(names, new Comparator<String>() {
@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<String> 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<String> 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.
2. Lambda Syntax
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).
2.1 Syntax Variations
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<String> 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<String> print2 = (message) -> System.out.println(message);
print2.accept("Lambda with parens");
// Output: Lambda with parens
// 4. Multiple parameters
BinaryOperator<Integer> add = (a, b) -> a + b;
System.out.println("Sum: " + add.apply(3, 7));
// Output: Sum: 10
// 5. Expression body - implicit return
Function<Integer, Integer> 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<Integer, String> 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<String> 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<String> 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
}
}2.2 Type Inference
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<String>, so a and b are String
Comparator<String> comp1 = (a, b) -> a.compareTo(b);
// You CAN specify types explicitly -- sometimes useful for clarity
Comparator<String> comp2 = (String a, String b) -> a.compareTo(b);
// IMPORTANT: You cannot mix -- either all types or no types
// Comparator<String> comp3 = (String a, b) -> a.compareTo(b); // COMPILE ERROR
// Type inference works with generics too
BiFunction<String, Integer, String> repeat = (text, times) -> text.repeat(times);
System.out.println(repeat.apply("Ha", 3));
// Output: HaHaHa
}
}3. Functional Interfaces
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.
3.1 What is a Functional Interface?
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:
- Exactly one abstract method (the SAM)
- Can have multiple
defaultandstaticmethods - Methods inherited from
Object(liketoString(),equals()) do not count - The
@FunctionalInterfaceannotation 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<F, T> {
T convert(F from);
@Override
String toString(); // From Object -- does NOT count as abstract
}3.2 Creating Custom Functional Interfaces
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<T> {
boolean validate(T item);
}
@FunctionalInterface
interface Transformer<T, R> {
R transform(T input);
}
@FunctionalInterface
interface TriFunction<A, B, C, R> {
R apply(A a, B b, C c);
}
public class CustomFunctionalInterfaces {
public static void main(String[] args) {
// Using custom Validator
Validator<String> 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<String, Integer> 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<Integer, Integer, Integer, Integer> 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
}
}3.3 Well-Known Functional Interfaces You Already Use
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.
4. Built-in Functional Interfaces
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.
4.1 Predicate<T> — Testing a Condition
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<Integer> 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<Integer> isEven = n -> n % 2 == 0;
Predicate<Integer> isPositiveAndEven = isPositive.and(isEven);
Predicate<Integer> isPositiveOrEven = isPositive.or(isEven);
Predicate<Integer> 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<String> names = List.of("Alice", "Bob", "Charlie", "Dave", "Eve");
Predicate<String> longerThan3 = name -> name.length() > 3;
Predicate<String> startsWithC = name -> name.startsWith("C");
List<String> 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<String> isAlice = Predicate.isEqual("Alice");
System.out.println("Is Alice: " + isAlice.test("Alice")); // true
System.out.println("Is Alice: " + isAlice.test(null)); // false
}
}4.2 Function<T, R> — Transforming Data
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<String, Integer> 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<String, String> toUpperCase = s -> s.toUpperCase();
Function<String, String> addExclamation = s -> s + "!";
Function<String, String> 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<Integer, Integer> multiplyBy2 = n -> n * 2;
Function<Integer, Integer> add10 = n -> n + 10;
// compose: add10 runs first, then multiplyBy2
Function<Integer, Integer> 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<Integer, Integer> 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<String, String> identity = Function.identity();
System.out.println(identity.apply("unchanged"));
// Output: unchanged
// Practical: build a text processing pipeline
Function<String, String> trim = String::trim;
Function<String, String> lower = String::toLowerCase;
Function<String, String> normalize = trim.andThen(lower).andThen(s -> s.replaceAll("\\s+", " "));
System.out.println("'" + normalize.apply(" Hello WORLD ") + "'");
// Output: 'hello world'
}
}4.3 Consumer<T> — Performing an Action
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<String> print = s -> System.out.println(s);
print.accept("Hello from Consumer!");
// Output: Hello from Consumer!
// Chaining consumers with andThen()
Consumer<String> toUpper = s -> System.out.println("Upper: " + s.toUpperCase());
Consumer<String> toLower = s -> System.out.println("Lower: " + s.toLowerCase());
Consumer<String> both = toUpper.andThen(toLower);
both.accept("Lambda");
// Output:
// Upper: LAMBDA
// Lower: lambda
// Practical: process a list of items
List<String> emails = List.of("alice@example.com", "bob@example.com", "charlie@example.com");
Consumer<String> validate = email -> {
if (!email.contains("@")) {
System.out.println("INVALID: " + email);
}
};
Consumer<String> sendWelcome = email -> System.out.println("Sending welcome email to: " + email);
Consumer<String> logAction = email -> System.out.println("Logged: processed " + email);
Consumer<String> 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
}
}4.4 Supplier<T> — Providing a Value
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<String> helloSupplier = () -> "Hello, World!";
System.out.println(helloSupplier.get());
// Output: Hello, World!
// Supplier for current timestamp
Supplier<LocalDateTime> now = () -> LocalDateTime.now();
System.out.println("Current time: " + now.get());
// Output: Current time: 2024-01-15T10:30:00.123
// Supplier as a factory
Supplier<Random> 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<Double> 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<String> defaultName = () -> "Anonymous";
String displayName = (name != null) ? name : defaultName.get();
System.out.println("Name: " + displayName);
// Output: Name: Anonymous
}
}4.5 UnaryOperator<T> and BinaryOperator<T>
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<String> toUpper = s -> s.toUpperCase();
System.out.println(toUpper.apply("lambda"));
// Output: LAMBDA
UnaryOperator<Integer> doubleIt = n -> n * 2;
System.out.println(doubleIt.apply(7));
// Output: 14
// UnaryOperator with List.replaceAll()
List<String> 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<Integer> 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<String> join = (a, b) -> a + ", " + b;
System.out.println(join.apply("Hello", "World"));
// Output: Hello, World
// BinaryOperator with reduce()
List<Integer> 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<String> longerString = BinaryOperator.maxBy(
(a, b) -> Integer.compare(a.length(), b.length())
);
System.out.println(longerString.apply("short", "much longer"));
// Output: much longer
}
}4.6 Bi-Variants: BiFunction, BiPredicate, BiConsumer
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<T, U, R> - takes two args, returns a result
BiFunction<String, Integer, String> repeat = (text, times) -> text.repeat(times);
System.out.println(repeat.apply("Ha", 3));
// Output: HaHaHa
// BiPredicate<T, U> - takes two args, returns boolean
BiPredicate<String, Integer> 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<T, U> - takes two args, returns nothing
BiConsumer<String, Integer> printEntry = (key, value) ->
System.out.println(key + " = " + value);
// BiConsumer is especially useful with Map.forEach()
Map<String, Integer> 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<String, Integer> 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}
}
}4.7 Complete Reference Table
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.
5. Lambda with Collections
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<String> 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<String, Integer> 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<Integer> 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<String> 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<String> 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<String, List<String>> 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<String, Integer> 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}
}
}6. Lambda with Streams
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<String> names = List.of("Alice", "Bob", "Charlie", "David", "Eve", "Alice");
// ========== filter() -- takes a Predicate ==========
// Lambda version
List<String> 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<Integer> 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<String> 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<Integer> 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<String> allNames = List.of("Alice", "Anna", "Bob", "Bill", "Charlie", "Chris");
Map<Character, List<String>> 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<String> 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<String> 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
}
}7. Variable Capture
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.
7.1 Effectively Final Variables
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<String> 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();
}
}7.2 Why This Restriction?
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.
7.3 Workarounds for Mutable State
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<Integer> 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<String> 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<String> 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]
}
}8. Lambda vs Anonymous Class
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.
8.1 Side-by-Side Comparison
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<String> names = Arrays.asList("Charlie", "Alice", "Bob");
// ========== Anonymous inner class ==========
names.sort(new Comparator<String>() {
@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<String> 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]
}
}8.2 Detailed Comparison Table
| 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 |
8.3 When to Use Each
Use a lambda when:
- The interface has exactly one abstract method (functional interface)
- The implementation is short (1-3 lines)
- You do not need
thisto refer to the implementation itself - You do not need to maintain state
Use an anonymous class when:
- The interface has multiple abstract methods
- You need
thisto refer to the implementation instance - You need instance fields to maintain state across method calls
- You want to override multiple methods from an abstract class
8.4 Migration Example
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class MigrationExample {
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of("Charlie", "Alice", "Bob", "David"));
// STEP 1: Original anonymous class
Collections.sort(names, new java.util.Comparator<String>() {
@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]
}
}9. Method References
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<String> words = List.of("hello", "world", "java", "lambda");
// ========== 1. Static method reference ==========
// Lambda: s -> Integer.parseInt(s)
// Method ref: Integer::parseInt
List<String> numberStrings = List.of("1", "2", "3", "4", "5");
List<Integer> 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<String> 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<String> 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<List<String>> listFactory = java.util.ArrayList::new;
List<String> 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<String, StringBuilder> 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.
10. Common Patterns
Lambdas are not just syntactic sugar — they enable cleaner implementations of well-known design patterns. Here are patterns you will use regularly.
10.1 Event Handling / Callbacks
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<T> {
private final List<Consumer<T>> listeners = new ArrayList<>();
public void on(Consumer<T> 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<String> 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
}
}10.2 Strategy Pattern
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<Double, Integer, Double> noDiscount =
(price, qty) -> price * qty;
BiFunction<Double, Integer, Double> percentageDiscount =
(price, qty) -> price * qty * 0.9; // 10% off
BiFunction<Double, Integer, Double> bulkDiscount =
(price, qty) -> qty >= 10 ? price * qty * 0.8 : price * qty; // 20% off for 10+
BiFunction<Double, Integer, Double> 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
}
}10.3 Decorator Pattern
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 <T, R> Function<T, R> withLogging(String name, Function<T, R> 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 <T, R> Function<T, R> withTiming(String name, Function<T, R> 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<String, String> reverseString = s ->
new StringBuilder(s).reverse().toString();
// Decorate with logging
Function<String, String> loggedReverse = withLogging("reverse", reverseString);
// Decorate with logging AND timing
Function<String, String> 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
}
}10.4 Lazy Evaluation
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<String> 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
}
}11. Common Mistakes
Even experienced developers make mistakes with lambdas. Here are the most common pitfalls and how to avoid them.
11.1 Checked Exceptions in Lambdas
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<T, R> {
R apply(T t) throws Exception;
}
// Wrapper method to convert a throwing function into a standard Function
static <T, R> Function<T, R> unchecked(ThrowingFunction<T, R> fn) {
return t -> {
try {
return fn.apply(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
public static void main(String[] args) {
List<String> 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
}
}11.2 Side Effects in Stream Lambdas
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<String> names = List.of("Alice", "Bob", "Charlie", "David");
// BAD: Modifying external list from inside map()
List<String> 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<String> 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
}
}11.3 Overly Complex Lambdas
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<String> filterBad(List<String> 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<String> filterGood(List<String> emails) {
return emails.stream()
.filter(ComplexLambdaMistake::isValidEmail) // Clean and readable
.collect(Collectors.toList());
}
public static void main(String[] args) {
List<String> 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]
}
}11.4 Forgetting the Functional Interface Requirement
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.
}
}11.5 Lambda Serialization Issues
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<T> {
boolean test(T t);
}
// Serializable functional interface
@FunctionalInterface
interface SerializablePredicate<T> extends Predicate<T>, Serializable {
}
public static void main(String[] args) {
// This lambda is NOT serializable
RegularPredicate<String> notSerializable = s -> s.length() > 5;
// This lambda IS serializable
SerializablePredicate<String> serializable = s -> s.length() > 5;
// Or use an intersection cast (less clean but avoids a custom interface)
Predicate<String> alsoSerializable = (Predicate<String> & Serializable) s -> s.length() > 5;
System.out.println("Test 'Lambda': " + serializable.test("Lambda"));
// Output: Test 'Lambda': true
}
}12. Best Practices
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<String, Object> 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 <T> List<T> filterBy(List<T> items, Predicate<T> criteria) {
return items.stream()
.filter(criteria)
.collect(Collectors.toList());
}
// BEST PRACTICE: Compose small, focused predicates
public static void main(String[] args) {
List<String> words = List.of("Lambda", "is", "a", "powerful", "feature", "in", "Java");
// GOOD: Small, focused predicates composed together
Predicate<String> longerThan2 = word -> word.length() > 2;
Predicate<String> startsWithLower = word -> Character.isLowerCase(word.charAt(0));
List<String> 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<String, List<String>> 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<String> 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]
}
}13. Complete Practical Example: Student Data Processing
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<String> courses;
Student(String name, String major, double gpa, int age, List<String> 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<String> 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<T> {
String generate(List<T> data);
}
// ========== Utility: generic filter + transform pipeline ==========
static <T, R> List<R> pipeline(List<T> data, Predicate<T> filter, Function<T, R> transform) {
return data.stream()
.filter(filter)
.map(transform)
.collect(Collectors.toList());
}
// ========== Main ==========
public static void main(String[] args) {
// Create sample data
List<Student> 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<Student> 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<Student> isCS = s -> s.getMajor().equals("Computer Science");
Predicate<Student> 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<Student, String> toNameUpper = s -> s.getName().toUpperCase();
List<String> 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<String, List<Student>> 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<String, DoubleSummaryStatistics> 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<String> 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<String, Long> 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.<String, Long>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<Student> honorRollReport = data -> {
StringBuilder sb = new StringBuilder();
sb.append("Honor Roll Report\n");
sb.append("=================\n");
List<Student> 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<Student> emailNotification = s ->
System.out.println(" [EMAIL] Congratulations " + s.getName() + "! You made the honor roll.");
Consumer<Student> smsNotification = s ->
System.out.println(" [SMS] " + s.getName() + ", check your email for honor roll details.");
Consumer<Student> logNotification = s ->
System.out.println(" [LOG] Notification sent to " + s.getName());
Consumer<Student> 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");
}
}Quick Reference
| 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 |