Java – Lambda Expression

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 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.

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 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
    }
}

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, 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
    }
}

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 default and static methods
  • Methods inherited from Object (like toString(), equals()) do not count
  • The @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
}

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 {
    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
    }
}

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 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
    }
}

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 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'
    }
}

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 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
    }
}

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 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
    }
}

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 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
    }
}

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 - 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}
    }
}

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 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}
    }
}

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 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
    }
}

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 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 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]
    }
}

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 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]
    }
}

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 this to 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 this to 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 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]
    }
}

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 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.

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 {
    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
    }
}

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 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
    }
}

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  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
    }
}

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 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 {
        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
    }
}

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 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
    }
}

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 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]
    }
}

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 {
        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
    }
}

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 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]
    }
}

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 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");
    }
}

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



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *