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

required
required


Lambda Expression

1. What Changed in Java 8?

Before Java 8, Java was a purely object-oriented language. Every piece of behavior — sorting a list, filtering a collection, handling a button click — had to be wrapped inside a class. If you wanted to pass a comparator to Collections.sort(), you created an anonymous inner class with five lines of boilerplate just to express one line of logic. Java 8 changed that.

Released in March 2014, Java 8 was the most significant update to the language since generics arrived in Java 5. It introduced lambda expressions, which brought functional programming capabilities to Java for the first time. This was not just a syntax convenience — it was a paradigm shift. Developers could now treat behavior as data: pass functions as arguments, return them from methods, and store them in variables.

Why this matters:

  • Eliminates boilerplate — An anonymous inner class with 5-7 lines becomes a single expression
  • Enables functional programming — Pass behavior (not just data) as method arguments
  • Powers the Stream API — filter, map, reduce, collect all depend on lambdas
  • Improves readability — Code reads closer to what it does rather than how it is wired up
  • Encourages immutability — Functional style promotes working with values rather than mutating state

The three pillars of Java 8’s functional programming support work together:

Feature Purpose Example
Lambda Expressions Define inline anonymous functions (a, b) -> a + b
Functional Interfaces Provide the type system for lambdas Predicate<T>, Function<T,R>
Method References Shorthand for lambdas that call existing methods String::toUpperCase

Here is the before-and-after that summarizes the entire shift:

import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class Java8ParadigmShift {
    public static void main(String[] args) {

        // ===== BEFORE Java 8: Anonymous inner class =====
        List names = Arrays.asList("Charlie", "Alice", "Bob");
        Collections.sort(names, new Comparator() {
            @Override
            public int compare(String a, String b) {
                return a.compareTo(b);
            }
        });
        System.out.println("Before Java 8: " + names);
        // Output: Before Java 8: [Alice, Bob, Charlie]

        // ===== Java 8: Lambda expression =====
        List names2 = Arrays.asList("Charlie", "Alice", "Bob");
        Collections.sort(names2, (a, b) -> a.compareTo(b));
        System.out.println("Lambda: " + names2);
        // Output: Lambda: [Alice, Bob, Charlie]

        // ===== Java 8: Method reference (even shorter) =====
        List names3 = Arrays.asList("Charlie", "Alice", "Bob");
        names3.sort(String::compareTo);
        System.out.println("Method ref: " + names3);
        // Output: Method ref: [Alice, Bob, Charlie]

        // ===== Java 8: List.sort() replaces Collections.sort() =====
        List names4 = Arrays.asList("Charlie", "Alice", "Bob");
        names4.sort(Comparator.naturalOrder());
        System.out.println("Comparator factory: " + names4);
        // Output: Comparator factory: [Alice, Bob, Charlie]
    }
}

Seven lines of anonymous class code reduced to a single expression. That is the power of Java 8.

2. Lambda Syntax Deep Dive

A lambda expression has three parts separated by the arrow operator ->:

(parameters) -> expression        // Single expression, implicit return
(parameters) -> { statements; }   // Block body, explicit return needed

The left side defines the input. The right side defines the output or action. The compiler infers parameter types, return type, and the functional interface from the context where the lambda is used. This is called target typing.

2.1 All Syntax Variations

Variation Syntax Example
No parameters () -> expression () -> System.out.println("Hello")
Single parameter (no parens) 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) (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.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public class LambdaSyntaxVariations {
    public static void main(String[] args) {

        // 1. No parameters
        Runnable greet = () -> System.out.println("Hello, Java 8!");
        greet.run();
        // Output: Hello, Java 8!

        // 2. Single parameter - parentheses optional
        Function upper = name -> name.toUpperCase();
        System.out.println(upper.apply("lambda"));
        // Output: LAMBDA

        // 3. Single parameter - with parentheses (same thing)
        Function upper2 = (name) -> name.toUpperCase();
        System.out.println(upper2.apply("expressions"));
        // Output: EXPRESSIONS

        // 4. Multiple parameters - parentheses required
        BiFunction add = (a, b) -> a + b;
        System.out.println("Sum: " + add.apply(10, 20));
        // Output: Sum: 30

        // 5. Expression body - implicit return
        Function square = x -> x * x;
        System.out.println("Square of 7: " + square.apply(7));
        // Output: Square of 7: 49

        // 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(classify.apply(-5));
        // Output: negative

        // 7. Block body - void (no return)
        Consumer logger = msg -> {
            String timestamp = java.time.LocalTime.now().toString();
            System.out.println("[" + timestamp + "] " + msg);
        };
        logger.accept("Application started");
        // Output: [14:30:22.123456] Application started

        // 8. Explicit parameter types
        BiFunction concat = (String a, String b) -> a + " " + b;
        System.out.println(concat.apply("Hello", "World"));
        // Output: Hello World
    }
}

2.2 Type Inference and Target Typing

The Java compiler determines the type of a lambda from the context in which it appears. This is called target typing. The same lambda expression can match different functional interfaces depending on where it is used:

import java.util.concurrent.Callable;
import java.util.function.Supplier;

public class TargetTyping {
    public static void main(String[] args) throws Exception {

        // Same lambda, different target types
        Supplier supplier = () -> "Hello from Supplier";
        Callable callable = () -> "Hello from Callable";

        System.out.println(supplier.get());
        // Output: Hello from Supplier

        System.out.println(callable.call());
        // Output: Hello from Callable

        // The compiler infers parameter types from context
        // No need to write (Integer a, Integer b) -> ...
        java.util.Comparator ascending = (a, b) -> a - b;
        java.util.Comparator descending = (a, b) -> b - a;

        java.util.List nums = new java.util.ArrayList<>(
            java.util.Arrays.asList(3, 1, 4, 1, 5)
        );
        nums.sort(ascending);
        System.out.println("Ascending: " + nums);
        // Output: Ascending: [1, 1, 3, 4, 5]

        nums.sort(descending);
        System.out.println("Descending: " + nums);
        // Output: Descending: [5, 4, 3, 1, 1]
    }
}

2.3 Effectively Final Variables

Lambdas can reference local variables from the enclosing scope, but those variables must be effectively final — meaning they are never reassigned after initialization. You do not need the final keyword explicitly, but the compiler enforces that the value does not change.

import java.util.function.Function;

public class EffectivelyFinal {
    public static void main(String[] args) {

        // This works - 'prefix' is effectively final
        String prefix = "Hello";
        Function greeter = name -> prefix + ", " + name + "!";
        System.out.println(greeter.apply("Alice"));
        // Output: Hello, Alice!

        // This would NOT compile:
        // String greeting = "Hi";
        // greeting = "Hello";  // reassignment makes it NOT effectively final
        // Function broken = name -> greeting + name;
        // Error: Variable used in lambda should be final or effectively final

        // Arrays and objects CAN be modified (the reference is final, not the contents)
        int[] counter = {0};
        Runnable increment = () -> counter[0]++;
        increment.run();
        increment.run();
        increment.run();
        System.out.println("Counter: " + counter[0]);
        // Output: Counter: 3
    }
}

3. Functional Interfaces

A functional interface is an interface with exactly one abstract method. This is also called a SAM (Single Abstract Method) interface. Every lambda expression in Java corresponds to a functional interface — the lambda provides the implementation of that single abstract method.

The @FunctionalInterface annotation is optional but strongly recommended. It tells the compiler to enforce the single-abstract-method rule. If someone accidentally adds a second abstract method, the compiler will produce an error immediately.

// Custom functional interface
@FunctionalInterface
public interface Transformer {
    T transform(T input);

    // Default methods are allowed - they are not abstract
    default Transformer andThen(Transformer after) {
        return input -> after.transform(this.transform(input));
    }

    // Static methods are allowed
    static  Transformer identity() {
        return input -> input;
    }

    // Methods from Object are allowed (toString, equals, hashCode)
    // They do NOT count as abstract methods
    String toString();
}

Rules for functional interfaces:

  • Exactly one abstract method
  • Can have any number of default methods
  • Can have any number of static methods
  • Can override methods from Object (toString, equals, hashCode) — these do not count
  • @FunctionalInterface is optional but recommended for compile-time safety

3.1 Java 8 Built-in Functional Interfaces

Java 8 added the java.util.function package with 43 functional interfaces. The seven most important ones cover the vast majority of use cases:

Interface Method Input Output Use Case
Predicate<T> test(T) T boolean Filtering, matching, validating
Function<T, R> apply(T) T R Transforming, mapping, converting
Consumer<T> accept(T) T void Side effects: print, log, save
Supplier<T> get() none T Factory, lazy evaluation, defaults
BiFunction<T, U, R> apply(T, U) T, U R Two-input transformation
UnaryOperator<T> apply(T) T T (same type) Modify and return same type
BinaryOperator<T> apply(T, T) T, T T (same type) Combining two values of same type

4. Each Built-in Interface in Detail

4.1 Predicate — Test a Condition

Predicate<T> takes an input and returns boolean. Think of it as a yes/no question about the input. It also provides composition methods: and(), or(), and negate() for combining predicates.

import java.util.Arrays;
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: is the number even?
        Predicate isEven = n -> n % 2 == 0;
        System.out.println("Is 4 even? " + isEven.test(4));
        // Output: Is 4 even? true
        System.out.println("Is 7 even? " + isEven.test(7));
        // Output: Is 7 even? false

        // Composing predicates with and(), or(), negate()
        Predicate isPositive = n -> n > 0;
        Predicate isEvenAndPositive = isEven.and(isPositive);
        Predicate isEvenOrPositive = isEven.or(isPositive);
        Predicate isOdd = isEven.negate();

        System.out.println("Is -4 even AND positive? " + isEvenAndPositive.test(-4));
        // Output: Is -4 even AND positive? false
        System.out.println("Is -4 even OR positive? " + isEvenOrPositive.test(-4));
        // Output: Is -4 even OR positive? true
        System.out.println("Is 7 odd? " + isOdd.test(7));
        // Output: Is 7 odd? true

        // Practical: filtering a list with composed predicates
        List numbers = Arrays.asList(-5, -2, 0, 3, 4, 7, 10, -8);
        List evenAndPositive = numbers.stream()
                .filter(isEven.and(isPositive))
                .collect(Collectors.toList());
        System.out.println("Even and positive: " + evenAndPositive);
        // Output: Even and positive: [4, 10]

        // String predicate: validation
        Predicate isNotEmpty = s -> s != null && !s.isEmpty();
        Predicate isEmail = s -> s.contains("@") && s.contains(".");
        Predicate isValidEmail = isNotEmpty.and(isEmail);

        System.out.println("Valid email? " + isValidEmail.test("user@example.com"));
        // Output: Valid email? true
        System.out.println("Valid email? " + isValidEmail.test("not-an-email"));
        // Output: Valid email? false

        // Predicate.isEqual() - static factory method
        Predicate isAdmin = Predicate.isEqual("ADMIN");
        System.out.println("Is ADMIN? " + isAdmin.test("ADMIN"));
        // Output: Is ADMIN? true
        System.out.println("Is USER? " + isAdmin.test("USER"));
        // Output: Is USER? false
    }
}

4.2 Function — Transform Input to Output

Function<T, R> takes one input of type T and produces an output of type R. It is the most general-purpose functional interface. The composition methods andThen() and compose() let you chain transformations into a pipeline.

import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

public class FunctionExamples {
    public static void main(String[] args) {

        // Basic function: string to its length
        Function length = String::length;
        System.out.println("Length of 'Lambda': " + length.apply("Lambda"));
        // Output: Length of 'Lambda': 6

        // andThen: apply this function, THEN apply the next
        Function trim = String::trim;
        Function toUpper = String::toUpperCase;
        Function cleanAndUpperCase = trim.andThen(toUpper);

        System.out.println(cleanAndUpperCase.apply("  hello world  "));
        // Output: HELLO WORLD

        // compose: apply the OTHER function first, THEN this one
        // compose is the reverse order of andThen
        Function doubleIt = x -> x * 2;
        Function addTen = x -> x + 10;

        // doubleIt.andThen(addTen) means: double first, then add 10
        System.out.println("Double then add 10: " + doubleIt.andThen(addTen).apply(5));
        // Output: Double then add 10: 20   (5*2=10, 10+10=20)

        // doubleIt.compose(addTen) means: add 10 first, then double
        System.out.println("Add 10 then double: " + doubleIt.compose(addTen).apply(5));
        // Output: Add 10 then double: 30   (5+10=15, 15*2=30)

        // Function.identity() - returns input unchanged
        Function identity = Function.identity();
        System.out.println(identity.apply("unchanged"));
        // Output: unchanged

        // Practical: build a transformation pipeline
        Function pipeline = ((Function) String::trim)
                .andThen(String::toLowerCase)
                .andThen(s -> s.replaceAll("\\s+", "-"))
                .andThen(s -> s.replaceAll("[^a-z0-9-]", ""));

        System.out.println(pipeline.apply("  Hello World! Java 8  "));
        // Output: hello-world-java-8

        // Using Function with streams
        List names = Arrays.asList("alice", "bob", "charlie");
        Function capitalize = s -> s.substring(0, 1).toUpperCase() + s.substring(1);
        List capitalized = names.stream()
                .map(capitalize)
                .collect(Collectors.toList());
        System.out.println(capitalized);
        // Output: [Alice, Bob, Charlie]
    }
}

4.3 Consumer — Perform an Action

Consumer<T> takes an input and returns nothing (void). It represents a side effect — printing, logging, saving to a database, sending a notification. The andThen() method chains multiple consumers.

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class ConsumerExamples {
    public static void main(String[] args) {

        // Basic consumer: print a value
        Consumer printer = System.out::println;
        printer.accept("Hello from Consumer!");
        // Output: Hello from Consumer!

        // Chaining consumers with andThen
        Consumer log = msg -> System.out.println("[LOG] " + msg);
        Consumer save = msg -> System.out.println("[SAVE] Persisting: " + msg);
        Consumer logAndSave = log.andThen(save);

        logAndSave.accept("User signed up");
        // Output: [LOG] User signed up
        // Output: [SAVE] Persisting: User signed up

        // Practical: process a list of users
        List users = Arrays.asList("Alice", "Bob", "Charlie");

        Consumer welcome = name -> System.out.println("Welcome, " + name + "!");
        Consumer assignRole = name -> System.out.println("Assigning default role to " + name);
        Consumer onboardUser = welcome.andThen(assignRole);

        System.out.println("--- Onboarding users ---");
        users.forEach(onboardUser);
        // Output:
        // --- Onboarding users ---
        // Welcome, Alice!
        // Assigning default role to Alice
        // Welcome, Bob!
        // Assigning default role to Bob
        // Welcome, Charlie!
        // Assigning default role to Charlie

        // BiConsumer: two inputs
        java.util.Map scores = new java.util.HashMap<>();
        scores.put("Alice", 95);
        scores.put("Bob", 87);
        scores.put("Charlie", 92);

        scores.forEach((name, score) ->
            System.out.println(name + " scored " + score)
        );
        // Output: Alice scored 95
        // Output: Bob scored 87
        // Output: Charlie scored 92
    }
}

4.4 Supplier — Provide a Value

Supplier<T> takes no input and returns a value. It is a factory or generator. Common uses include providing default values, lazy initialization, and deferred computation.

import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Stream;

public class SupplierExamples {
    public static void main(String[] args) {

        // Basic supplier: return a constant
        Supplier greeting = () -> "Hello, World!";
        System.out.println(greeting.get());
        // Output: Hello, World!

        // Supplier as a factory
        Supplier sbFactory = StringBuilder::new;
        StringBuilder sb1 = sbFactory.get();
        StringBuilder sb2 = sbFactory.get();
        sb1.append("First");
        sb2.append("Second");
        System.out.println(sb1 + ", " + sb2);
        // Output: First, Second

        // Lazy evaluation with Supplier
        Supplier randomValue = () -> Math.random();
        // Nothing computed yet - only computed when get() is called
        System.out.println("Random 1: " + randomValue.get());
        System.out.println("Random 2: " + randomValue.get());
        // Output: Random 1: 0.7234... (varies)
        // Output: Random 2: 0.1456... (varies)

        // Generate infinite stream from Supplier
        Supplier diceRoll = () -> new Random().nextInt(6) + 1;
        System.out.print("Five dice rolls: ");
        Stream.generate(diceRoll)
              .limit(5)
              .forEach(n -> System.out.print(n + " "));
        System.out.println();
        // Output: Five dice rolls: 3 1 5 2 6 (varies)

        // Practical: providing default values
        String configValue = null;
        Supplier defaultConfig = () -> "localhost:8080";
        String result = configValue != null ? configValue : defaultConfig.get();
        System.out.println("Config: " + result);
        // Output: Config: localhost:8080
    }
}

4.5 BiFunction — Two Inputs, One Output

BiFunction<T, U, R> takes two inputs and produces one output. It is useful when a transformation needs two pieces of data. It has andThen() but no compose() (since compose would need a function that returns two values, which Java does not support).

import java.util.function.BiFunction;

public class BiFunctionExamples {
    public static void main(String[] args) {

        // Basic: combine two values
        BiFunction fullName =
            (first, last) -> first + " " + last;
        System.out.println(fullName.apply("John", "Doe"));
        // Output: John Doe

        // Math operation
        BiFunction power =
            (base, exponent) -> Math.pow(base, exponent);
        System.out.println("2^10 = " + power.apply(2, 10));
        // Output: 2^10 = 1024.0

        // andThen: transform the result
        BiFunction greet =
            (greeting, name) -> greeting + ", " + name;
        String result = greet.andThen(String::toUpperCase).apply("hello", "world");
        System.out.println(result);
        // Output: HELLO, WORLD

        // Practical: calculate discounted price
        BiFunction priceWithDiscount =
            (price, discountPercent) -> {
                double discounted = price * (1 - discountPercent / 100);
                return String.format("$%.2f (%.0f%% off $%.2f)",
                    discounted, discountPercent, price);
            };

        System.out.println(priceWithDiscount.apply(100.0, 25.0));
        // Output: $75.00 (25% off $100.00)
        System.out.println(priceWithDiscount.apply(49.99, 10.0));
        // Output: $44.99 (10% off $49.99)
    }
}

4.6 UnaryOperator — Same Type In, Same Type Out

UnaryOperator<T> extends Function<T, T>. It takes a value and returns a value of the same type. Use it when you are modifying a value without changing its type — formatting a string, incrementing a number, transforming a list element in place.

import java.util.Arrays;
import java.util.List;
import java.util.function.UnaryOperator;

public class UnaryOperatorExamples {
    public static void main(String[] args) {

        // Basic: increment a number
        UnaryOperator increment = x -> x + 1;
        System.out.println(increment.apply(10));
        // Output: 11

        // String transformation
        UnaryOperator addExclamation = s -> s + "!";
        UnaryOperator toUpper = String::toUpperCase;
        UnaryOperator shout = toUpper.andThen(addExclamation)::apply;

        System.out.println(shout.apply("hello"));
        // Output: HELLO!

        // Practical: List.replaceAll() uses UnaryOperator
        List names = Arrays.asList("alice", "bob", "charlie");
        names.replaceAll(String::toUpperCase);
        System.out.println(names);
        // Output: [ALICE, BOB, CHARLIE]

        // UnaryOperator.identity() - returns input unchanged
        UnaryOperator noChange = UnaryOperator.identity();
        System.out.println(noChange.apply("same"));
        // Output: same

        // Chaining: build text formatter
        UnaryOperator trim = String::trim;
        UnaryOperator lower = String::toLowerCase;
        UnaryOperator format = trim.andThen(lower)::apply;

        System.out.println(format.apply("  HELLO WORLD  "));
        // Output: hello world
    }
}

4.7 BinaryOperator — Combine Two Values of Same Type

BinaryOperator<T> extends BiFunction<T, T, T>. It takes two inputs of the same type and returns a result of that same type. It is the ideal type for reduction operations like summing, finding max/min, or merging.

import java.util.Arrays;
import java.util.List;
import java.util.function.BinaryOperator;

public class BinaryOperatorExamples {
    public static void main(String[] args) {

        // Basic: add two integers
        BinaryOperator add = (a, b) -> a + b;
        System.out.println("Sum: " + add.apply(10, 20));
        // Output: Sum: 30

        // Concatenate strings
        BinaryOperator concat = (a, b) -> a + " " + b;
        System.out.println(concat.apply("Hello", "World"));
        // Output: Hello World

        // BinaryOperator.maxBy() and minBy() - factory methods
        BinaryOperator max = BinaryOperator.maxBy(Integer::compareTo);
        BinaryOperator min = BinaryOperator.minBy(Integer::compareTo);
        System.out.println("Max of 10, 20: " + max.apply(10, 20));
        // Output: Max of 10, 20: 20
        System.out.println("Min of 10, 20: " + min.apply(10, 20));
        // Output: Min of 10, 20: 10

        // Practical: reduce a list
        List numbers = Arrays.asList(1, 2, 3, 4, 5);
        BinaryOperator sum = Integer::sum;
        int total = numbers.stream().reduce(0, sum);
        System.out.println("Total: " + total);
        // Output: Total: 15

        // Practical: find longest string
        List words = Arrays.asList("java", "lambda", "expression", "functional");
        BinaryOperator longer = BinaryOperator.maxBy(
            java.util.Comparator.comparingInt(String::length)
        );
        String longest = words.stream().reduce(longer).orElse("");
        System.out.println("Longest word: " + longest);
        // Output: Longest word: expression
    }
}

5. Lambda with Collections

Java 8 added new methods directly to the Collection framework interfaces that accept lambdas. These let you work with collections without streams, using simple method calls on the collection itself.

5.1 Iterable.forEach()

The forEach() method was added to Iterable (the parent of all collections). It takes a Consumer and executes it for each element.

import java.util.Arrays;
import java.util.List;
import java.util.HashMap;
import java.util.Map;

public class ForEachExamples {
    public static void main(String[] args) {

        // List.forEach()
        List languages = Arrays.asList("Java", "Python", "JavaScript");
        languages.forEach(lang -> System.out.println("I know " + lang));
        // Output: I know Java
        // Output: I know Python
        // Output: I know JavaScript

        // Method reference shorthand
        languages.forEach(System.out::println);
        // Output: Java
        // Output: Python
        // Output: JavaScript

        // Map.forEach() - takes BiConsumer
        Map ages = new HashMap<>();
        ages.put("Alice", 30);
        ages.put("Bob", 25);
        ages.put("Charlie", 35);

        ages.forEach((name, age) ->
            System.out.println(name + " is " + age + " years old")
        );
        // Output: Alice is 30 years old
        // Output: Bob is 25 years old
        // Output: Charlie is 35 years old
    }
}

5.2 Collection.removeIf(), List.replaceAll(), List.sort()

These methods modify the collection in place using lambda predicates and functions.

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class CollectionLambdaMethods {
    public static void main(String[] args) {

        // ===== removeIf(Predicate) - remove elements that match =====
        List numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
        numbers.removeIf(n -> n % 2 == 0);  // Remove even numbers
        System.out.println("Odds only: " + numbers);
        // Output: Odds only: [1, 3, 5, 7, 9]

        // Remove empty strings
        List items = new ArrayList<>(Arrays.asList("apple", "", "banana", "", "cherry"));
        items.removeIf(String::isEmpty);
        System.out.println("Non-empty: " + items);
        // Output: Non-empty: [apple, banana, cherry]

        // ===== replaceAll(UnaryOperator) - transform every element =====
        List names = new ArrayList<>(Arrays.asList("alice", "bob", "charlie"));
        names.replaceAll(String::toUpperCase);
        System.out.println("Uppercased: " + names);
        // Output: Uppercased: [ALICE, BOB, CHARLIE]

        List prices = new ArrayList<>(Arrays.asList(10.0, 20.0, 30.0));
        prices.replaceAll(p -> p * 1.1);  // 10% price increase
        System.out.println("After 10% increase: " + prices);
        // Output: After 10% increase: [11.0, 22.0, 33.0]

        // ===== sort(Comparator) - sort with lambda =====
        List fruits = new ArrayList<>(Arrays.asList("banana", "apple", "cherry", "date"));
        fruits.sort((a, b) -> a.compareTo(b));
        System.out.println("Alphabetical: " + fruits);
        // Output: Alphabetical: [apple, banana, cherry, date]

        // Sort by length, then alphabetically
        fruits.sort(Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()));
        System.out.println("By length: " + fruits);
        // Output: By length: [date, apple, banana, cherry]

        // Reverse sort
        fruits.sort(Comparator.reverseOrder());
        System.out.println("Reverse: " + fruits);
        // Output: Reverse: [date, cherry, banana, apple]
    }
}

5.3 Map Methods: computeIfAbsent, merge, getOrDefault

Java 8 added several powerful lambda-accepting methods to Map that simplify common patterns.

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MapLambdaMethods {
    public static void main(String[] args) {

        // ===== computeIfAbsent(key, mappingFunction) =====
        // If the key is absent, compute and insert the value
        Map> groups = new HashMap<>();

        // BEFORE Java 8:
        // if (!groups.containsKey("fruits")) {
        //     groups.put("fruits", new ArrayList<>());
        // }
        // groups.get("fruits").add("apple");

        // AFTER Java 8:
        groups.computeIfAbsent("fruits", k -> new ArrayList<>()).add("apple");
        groups.computeIfAbsent("fruits", k -> new ArrayList<>()).add("banana");
        groups.computeIfAbsent("vegetables", k -> new ArrayList<>()).add("carrot");
        System.out.println(groups);
        // Output: {fruits=[apple, banana], vegetables=[carrot]}

        // ===== merge(key, value, remappingFunction) =====
        // Combine old and new values
        Map wordCount = new HashMap<>();
        String[] words = {"hello", "world", "hello", "java", "hello", "world"};

        for (String word : words) {
            wordCount.merge(word, 1, Integer::sum);
        }
        System.out.println("Word counts: " + wordCount);
        // Output: Word counts: {java=1, world=2, hello=3}

        // ===== getOrDefault(key, defaultValue) =====
        Map config = new HashMap<>();
        config.put("host", "localhost");
        config.put("port", "8080");

        String host = config.getOrDefault("host", "127.0.0.1");
        String timeout = config.getOrDefault("timeout", "30");
        System.out.println("Host: " + host + ", Timeout: " + timeout);
        // Output: Host: localhost, Timeout: 30

        // ===== replaceAll(BiFunction) =====
        Map scores = new HashMap<>();
        scores.put("Alice", 85);
        scores.put("Bob", 92);
        scores.put("Charlie", 78);

        // Add 5 bonus points to everyone
        scores.replaceAll((name, score) -> score + 5);
        System.out.println("After bonus: " + scores);
        // Output: After bonus: {Alice=90, Bob=97, Charlie=83}

        // ===== compute(key, BiFunction) =====
        Map inventory = new HashMap<>();
        inventory.put("apples", 10);
        inventory.compute("apples", (key, val) -> val == null ? 1 : val + 5);
        inventory.compute("oranges", (key, val) -> val == null ? 1 : val + 5);
        System.out.println("Inventory: " + inventory);
        // Output: Inventory: {oranges=1, apples=15}
    }
}

6. Lambda Scope and Variable Capture

Lambdas have access to variables from the enclosing scope. Understanding exactly what they can and cannot access is critical for writing correct lambda code.

6.1 What Lambdas Can Access

Variable Type Can Access? Can Modify? Notes
Local variables Yes No Must be effectively final
Method parameters Yes No Must be effectively final
Instance fields (this.field) Yes Yes Captured via this reference
Static fields Yes Yes Accessed directly
import java.util.function.IntSupplier;
import java.util.function.Supplier;

public class LambdaScope {

    private int instanceCounter = 0;
    private static int staticCounter = 0;

    public void demonstrateScope() {
        int localVar = 10;  // effectively final - never reassigned

        // 1. Accessing local variable (must be effectively final)
        Supplier getLocal = () -> localVar;
        System.out.println("Local: " + getLocal.get());
        // Output: Local: 10

        // This would NOT compile:
        // localVar = 20;  // Makes localVar not effectively final
        // Supplier broken = () -> localVar;

        // 2. Accessing and MODIFYING instance fields - allowed
        Runnable incrementInstance = () -> instanceCounter++;
        incrementInstance.run();
        incrementInstance.run();
        System.out.println("Instance counter: " + instanceCounter);
        // Output: Instance counter: 2

        // 3. Accessing and MODIFYING static fields - allowed
        Runnable incrementStatic = () -> staticCounter++;
        incrementStatic.run();
        incrementStatic.run();
        incrementStatic.run();
        System.out.println("Static counter: " + staticCounter);
        // Output: Static counter: 3
    }

    public static void main(String[] args) {
        new LambdaScope().demonstrateScope();
    }
}

6.2 The ‘this’ Keyword in Lambdas

A critical difference between lambdas and anonymous classes is what this refers to. In a lambda, this refers to the enclosing class. In an anonymous class, this refers to the anonymous class itself.

public class ThisInLambda {

    private String name = "Enclosing class";

    public void demonstrate() {

        // Lambda: 'this' refers to ThisInLambda instance
        Runnable lambdaRunnable = () -> {
            System.out.println("Lambda this: " + this.name);
            System.out.println("Lambda this class: " + this.getClass().getSimpleName());
        };

        // Anonymous class: 'this' refers to the anonymous class instance
        Runnable anonRunnable = new Runnable() {
            @Override
            public void run() {
                // 'this' here refers to the anonymous Runnable
                System.out.println("Anon this class: " + this.getClass().getSimpleName());
                // To access the enclosing class, use EnclosingClass.this
                System.out.println("Anon enclosing: " + ThisInLambda.this.name);
            }
        };

        lambdaRunnable.run();
        // Output: Lambda this: Enclosing class
        // Output: Lambda this class: ThisInLambda

        anonRunnable.run();
        // Output: Anon this class: ThisInLambda$1
        // Output: Anon enclosing: Enclosing class
    }

    public static void main(String[] args) {
        new ThisInLambda().demonstrate();
    }
}

6.3 Workaround for the Effectively Final Restriction

If you need to accumulate state inside a lambda, you cannot use a plain int or String variable. Instead, use a mutable container like an array, AtomicInteger, or a custom holder object.

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class EffectivelyFinalWorkarounds {
    public static void main(String[] args) {
        List names = Arrays.asList("Alice", "Bob", "Charlie");

        // Approach 1: Single-element array
        int[] count1 = {0};
        names.forEach(name -> count1[0]++);
        System.out.println("Array counter: " + count1[0]);
        // Output: Array counter: 3

        // Approach 2: AtomicInteger (preferred, thread-safe)
        AtomicInteger count2 = new AtomicInteger(0);
        names.forEach(name -> count2.incrementAndGet());
        System.out.println("Atomic counter: " + count2.get());
        // Output: Atomic counter: 3

        // Approach 3: Use streams instead (cleanest solution)
        long count3 = names.stream().count();
        System.out.println("Stream count: " + count3);
        // Output: Stream count: 3

        // Approach 4: StringBuilder for string accumulation
        StringBuilder sb = new StringBuilder();
        names.forEach(name -> sb.append(name).append(", "));
        System.out.println("Accumulated: " + sb.toString());
        // Output: Accumulated: Alice, Bob, Charlie,
    }
}

7. Lambda vs Anonymous Classes

Lambdas and anonymous classes both create implementations of interfaces on the fly. However, they differ in important ways beyond just syntax.

7.1 Comparison Table

Feature Anonymous Class Lambda Expression
Syntax Verbose (class + method) Concise (arrow notation)
Can implement Any interface (including multi-method) or extend a class Only functional interfaces (single abstract method)
this keyword Refers to the anonymous class instance Refers to the enclosing class instance
Has own state Yes (can have fields) No (stateless, captures from enclosing scope)
Compiled to Separate .class file (Outer$1.class) invokedynamic bytecode (no extra class file)
Performance New class loaded per instantiation Lightweight, JVM can optimize via method handles
Serializable? Yes (if implements Serializable) Only with explicit cast
Shadowing Can shadow enclosing scope variables Cannot shadow (shares enclosing scope)

7.2 Migration Examples

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class AnonClassToLambda {
    public static void main(String[] args) {

        // ===== Example 1: Runnable =====
        // Anonymous class
        Runnable oldWay = new Runnable() {
            @Override
            public void run() {
                System.out.println("Running the old way");
            }
        };
        // Lambda
        Runnable newWay = () -> System.out.println("Running the new way");

        oldWay.run();  // Output: Running the old way
        newWay.run();  // Output: Running the new way

        // ===== Example 2: Comparator =====
        List names = new ArrayList<>();
        names.add("Charlie");
        names.add("Alice");
        names.add("Bob");

        // Anonymous class
        Collections.sort(names, new java.util.Comparator() {
            @Override
            public int compare(String a, String b) {
                return a.length() - b.length();
            }
        });
        System.out.println("Anonymous: " + names);
        // Output: Anonymous: [Bob, Alice, Charlie]

        // Lambda
        Collections.sort(names, (a, b) -> a.length() - b.length());
        System.out.println("Lambda: " + names);
        // Output: Lambda: [Bob, Alice, Charlie]

        // Method reference + Comparator factory (cleanest)
        names.sort(java.util.Comparator.comparingInt(String::length));
        System.out.println("Factory: " + names);
        // Output: Factory: [Bob, Alice, Charlie]

        // ===== Example 3: ActionListener (event handling) =====
        // Anonymous class (Swing)
        // button.addActionListener(new ActionListener() {
        //     @Override
        //     public void actionPerformed(ActionEvent e) {
        //         System.out.println("Button clicked!");
        //     }
        // });

        // Lambda
        // button.addActionListener(e -> System.out.println("Button clicked!"));

        // ===== Example 4: Thread =====
        // Anonymous class
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Thread via anonymous class: " + Thread.currentThread().getName());
            }
        });
        // Lambda
        Thread t2 = new Thread(() ->
            System.out.println("Thread via lambda: " + Thread.currentThread().getName())
        );

        t1.start();
        t2.start();
        // Output: Thread via anonymous class: Thread-0
        // Output: Thread via lambda: Thread-1
    }
}

7.3 When You Still Need Anonymous Classes

Lambdas cannot replace anonymous classes in every situation. Use an anonymous class when:

  • The interface has more than one abstract method (not a functional interface)
  • You need to extend a class (not implement an interface)
  • You need the anonymous instance to have its own state (fields)
  • You need this to refer to the anonymous instance itself

8. Common Patterns with Lambdas

8.1 Callbacks

Lambdas make the callback pattern clean and readable. Instead of creating a named class for each callback, define the behavior inline.

import java.util.function.Consumer;

public class CallbackPattern {

    // Method that accepts a callback
    static void fetchData(String url, Consumer onSuccess, Consumer onError) {
        try {
            // Simulate fetching data
            if (url.startsWith("http")) {
                String data = "{\"status\": \"ok\", \"source\": \"" + url + "\"}";
                onSuccess.accept(data);
            } else {
                throw new IllegalArgumentException("Invalid URL: " + url);
            }
        } catch (Exception e) {
            onError.accept(e.getMessage());
        }
    }

    public static void main(String[] args) {

        // Pass lambdas as callbacks
        fetchData("http://api.example.com",
            data -> System.out.println("Success: " + data),
            error -> System.out.println("Error: " + error)
        );
        // Output: Success: {"status": "ok", "source": "http://api.example.com"}

        fetchData("invalid-url",
            data -> System.out.println("Success: " + data),
            error -> System.out.println("Error: " + error)
        );
        // Output: Error: Invalid URL: invalid-url
    }
}

8.2 Strategy Pattern

The Strategy pattern defines a family of algorithms and lets you swap them at runtime. Before Java 8, each strategy was a separate class. With lambdas, you define strategies inline.

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class StrategyPattern {

    // The "strategy" is just a Predicate
    static  List filter(List items, Predicate strategy) {
        return items.stream().filter(strategy).collect(Collectors.toList());
    }

    public static void main(String[] args) {
        List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // Different strategies passed as lambdas
        List evens = filter(numbers, n -> n % 2 == 0);
        List odds = filter(numbers, n -> n % 2 != 0);
        List greaterThan5 = filter(numbers, n -> n > 5);

        System.out.println("Evens: " + evens);
        // Output: Evens: [2, 4, 6, 8, 10]
        System.out.println("Odds: " + odds);
        // Output: Odds: [1, 3, 5, 7, 9]
        System.out.println("Greater than 5: " + greaterThan5);
        // Output: Greater than 5: [6, 7, 8, 9, 10]

        // Compose strategies
        Predicate evenAndGreaterThan5 = ((Predicate) n -> n % 2 == 0).and(n -> n > 5);
        System.out.println("Even AND > 5: " + filter(numbers, evenAndGreaterThan5));
        // Output: Even AND > 5: [6, 8, 10]

        // Store strategies in variables for reuse
        Predicate isShort = s -> s.length() <= 4;
        Predicate startsWithJ = s -> s.startsWith("J");

        List langs = Arrays.asList("Java", "Python", "JavaScript", "Go", "C", "Rust");
        System.out.println("Short: " + filter(langs, isShort));
        // Output: Short: [Java, Go, C, Rust]
        System.out.println("Starts with J: " + filter(langs, startsWithJ));
        // Output: Starts with J: [Java, JavaScript]
    }
}

8.3 Lazy Evaluation

Lambdas enable lazy evaluation — the computation is not performed until the result is actually needed. This is useful for expensive operations where the result might not always be required.

import java.util.function.Supplier;

public class LazyEvaluation {

    static String expensiveOperation() {
        System.out.println("  [Computing expensive result...]");
        // Simulate expensive computation
        return "expensive result";
    }

    // Eager: always computes the default, even if not needed
    static String getValueEager(String value, String defaultValue) {
        return value != null ? value : defaultValue;
    }

    // Lazy: only computes default if value is null
    static String getValueLazy(String value, Supplier defaultSupplier) {
        return value != null ? value : defaultSupplier.get();
    }

    public static void main(String[] args) {
        String cached = "cached result";

        // Eager: expensiveOperation() is ALWAYS called
        System.out.println("--- Eager (value exists) ---");
        String eager = getValueEager(cached, expensiveOperation());
        System.out.println("Result: " + eager);
        // Output:
        // --- Eager (value exists) ---
        //   [Computing expensive result...]
        // Result: cached result

        // Lazy: expensiveOperation() is NOT called when value exists
        System.out.println("--- Lazy (value exists) ---");
        String lazy = getValueLazy(cached, () -> expensiveOperation());
        System.out.println("Result: " + lazy);
        // Output:
        // --- Lazy (value exists) ---
        // Result: cached result

        // Lazy: expensiveOperation() IS called when value is null
        System.out.println("--- Lazy (value is null) ---");
        String lazy2 = getValueLazy(null, () -> expensiveOperation());
        System.out.println("Result: " + lazy2);
        // Output:
        // --- Lazy (value is null) ---
        //   [Computing expensive result...]
        // Result: expensive result
    }
}

8.4 Custom Functional Interfaces

While the built-in interfaces cover most cases, sometimes you need a custom one. Common scenarios: checked exceptions, three or more parameters, or domain-specific naming.

import java.io.IOException;
import java.util.function.Function;

public class CustomFunctionalInterfaces {

    // Custom interface for operations that throw checked exceptions
    @FunctionalInterface
    interface ThrowingFunction {
        R apply(T input) throws Exception;

        // Convert to standard Function with exception wrapping
        static  Function unchecked(ThrowingFunction f) {
            return input -> {
                try {
                    return f.apply(input);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            };
        }
    }

    // Custom interface with three parameters
    @FunctionalInterface
    interface TriFunction {
        R apply(A a, B b, C c);
    }

    // Domain-specific interface
    @FunctionalInterface
    interface Validator {
        ValidationResult validate(T item);
    }

    record ValidationResult(boolean valid, String message) {
        static ValidationResult ok() { return new ValidationResult(true, "Valid"); }
        static ValidationResult error(String msg) { return new ValidationResult(false, msg); }
    }

    public static void main(String[] args) {

        // ThrowingFunction: handle checked exceptions in lambdas
        ThrowingFunction parse = Integer::parseInt;
        Function safeParse = ThrowingFunction.unchecked(parse);
        System.out.println("Parsed: " + safeParse.apply("42"));
        // Output: Parsed: 42

        // TriFunction: three inputs
        TriFunction formatUser =
            (first, last, age) -> first + " " + last + " (age " + age + ")";
        System.out.println(formatUser.apply("John", "Doe", 30));
        // Output: John Doe (age 30)

        // Domain validator
        Validator emailValidator = email -> {
            if (email == null || email.isEmpty()) {
                return ValidationResult.error("Email cannot be empty");
            }
            if (!email.contains("@")) {
                return ValidationResult.error("Email must contain @");
            }
            return ValidationResult.ok();
        };

        System.out.println(emailValidator.validate("user@example.com"));
        // Output: ValidationResult[valid=true, message=Valid]
        System.out.println(emailValidator.validate("bad-email"));
        // Output: ValidationResult[valid=false, message=Email must contain @]
    }
}

8.5 Event Listeners and Handlers

Lambdas dramatically simplify GUI and event-driven code where you previously needed verbose anonymous classes for every event handler.

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

public class EventListenerPattern {

    // Simple event system using lambdas
    static class EventEmitter {
        private final List> listeners = new ArrayList<>();

        void on(Consumer listener) {
            listeners.add(listener);
        }

        void emit(T event) {
            listeners.forEach(listener -> listener.accept(event));
        }
    }

    public static void main(String[] args) {

        // Create an emitter for String events
        EventEmitter userEvents = new EventEmitter<>();

        // Register listeners with lambdas
        userEvents.on(event -> System.out.println("[Logger] " + event));
        userEvents.on(event -> System.out.println("[Analytics] Tracking: " + event));
        userEvents.on(event -> {
            if (event.contains("signup")) {
                System.out.println("[Email] Sending welcome email");
            }
        });

        // Emit events
        userEvents.emit("user.signup");
        // Output: [Logger] user.signup
        // Output: [Analytics] Tracking: user.signup
        // Output: [Email] Sending welcome email

        System.out.println();
        userEvents.emit("user.login");
        // Output: [Logger] user.login
        // Output: [Analytics] Tracking: user.login
    }
}

9. Best Practices

Writing good lambda code requires discipline. These best practices come from real-world Java 8+ production codebases.

9.1 Keep Lambdas Short

If a lambda exceeds 3-4 lines, extract it into a named method and use a method reference. Long lambdas defeat the readability purpose.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class KeepLambdasShort {

    // BAD: Lambda is too long -- hard to read inline
    static List processNamesBad(List names) {
        return names.stream()
                .filter(name -> {
                    if (name == null || name.isEmpty()) return false;
                    if (name.length() < 2) return false;
                    if (!Character.isUpperCase(name.charAt(0))) return false;
                    return true;
                })
                .collect(Collectors.toList());
    }

    // GOOD: Extract to a named method, use method reference
    static boolean isValidName(String name) {
        if (name == null || name.isEmpty()) return false;
        if (name.length() < 2) return false;
        if (!Character.isUpperCase(name.charAt(0))) return false;
        return true;
    }

    static List processNamesGood(List names) {
        return names.stream()
                .filter(KeepLambdasShort::isValidName)
                .collect(Collectors.toList());
    }

    public static void main(String[] args) {
        List names = Arrays.asList("Alice", "", "b", "Charlie", null, "David");
        System.out.println(processNamesGood(names));
        // Output: [Alice, Charlie, David]
    }
}

9.2 Use Method References When Possible

If your lambda simply calls an existing method, use a method reference instead. It is shorter and communicates intent more clearly.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MethodReferencePreference {
    public static void main(String[] args) {
        List names = Arrays.asList("Alice", "Bob", "Charlie");

        // Lambda (ok, but wordy)
        names.forEach(name -> System.out.println(name));
        // Method reference (better)
        names.forEach(System.out::println);

        // Lambda
        List upper1 = names.stream()
                .map(s -> s.toUpperCase())
                .collect(Collectors.toList());
        // Method reference (better)
        List upper2 = names.stream()
                .map(String::toUpperCase)
                .collect(Collectors.toList());

        // Lambda
        List lengths1 = names.stream()
                .map(s -> s.length())
                .collect(Collectors.toList());
        // Method reference (better)
        List lengths2 = names.stream()
                .map(String::length)
                .collect(Collectors.toList());

        System.out.println(upper2);
        // Output: [ALICE, BOB, CHARLIE]
        System.out.println(lengths2);
        // Output: [5, 3, 7]
    }
}

9.3 Avoid Side Effects

Lambdas used in streams and functional-style code should be pure functions — they should produce a result based on input without modifying external state. Side effects in lambdas make code harder to reason about and can cause bugs in parallel streams.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class AvoidSideEffects {
    public static void main(String[] args) {
        List names = Arrays.asList("Alice", "Bob", "Charlie");

        // BAD: Side effect inside a lambda (modifying external list)
        List resultsBad = new java.util.ArrayList<>();
        names.stream()
                .filter(n -> n.length() > 3)
                .forEach(n -> resultsBad.add(n));  // Side effect!
        System.out.println("Bad approach: " + resultsBad);
        // Output: Bad approach: [Alice, Charlie]

        // GOOD: Pure functional style (collect result)
        List resultsGood = names.stream()
                .filter(n -> n.length() > 3)
                .collect(Collectors.toList());
        System.out.println("Good approach: " + resultsGood);
        // Output: Good approach: [Alice, Charlie]

        // BAD: Side effect modifying counter (NOT thread-safe with parallel streams)
        int[] count = {0};
        names.parallelStream().forEach(n -> count[0]++);  // Race condition!

        // GOOD: Use stream operations
        long goodCount = names.stream().count();
        System.out.println("Count: " + goodCount);
        // Output: Count: 3
    }
}

9.4 Use Meaningful Parameter Names

Single-letter parameter names are fine for simple lambdas, but use descriptive names when the context is not obvious.

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class MeaningfulParamNames {
    public static void main(String[] args) {

        // OK for simple cases - single letter is fine
        List numbers = Arrays.asList(1, 2, 3, 4, 5);
        numbers.stream().filter(n -> n > 3).forEach(System.out::println);

        // BAD: unclear what a, b, c represent
        // (a, b) -> a.getLastName().compareTo(b.getLastName())

        // GOOD: descriptive names make the comparison obvious
        // (employee1, employee2) -> employee1.getLastName().compareTo(employee2.getLastName())

        // Practical example with meaningful names
        List emails = Arrays.asList(
            "alice@company.com", "bob@gmail.com",
            "charlie@company.com", "diana@yahoo.com"
        );

        // BAD: what does 'e' represent?
        List bad = emails.stream().filter(e -> e.endsWith("@company.com"))
                .collect(Collectors.toList());

        // GOOD: clear meaning
        List companyEmails = emails.stream()
                .filter(email -> email.endsWith("@company.com"))
                .collect(Collectors.toList());

        System.out.println("Company emails: " + companyEmails);
        // Output: Company emails: [alice@company.com, charlie@company.com]
    }
}

9.5 Best Practices Summary

Practice Do Don’t
Length Keep lambdas to 1-3 lines Write 10+ line lambdas inline
Method references String::toUpperCase s -> s.toUpperCase()
Side effects .collect(Collectors.toList()) .forEach(list::add)
Parameter names employee -> employee.getSalary() e -> e.getSalary() in complex contexts
Type declarations Let the compiler infer types (String s) -> s.length() when unnecessary
Parentheses x -> x * 2 for single param (x) -> x * 2 (unnecessary parens)
Return statement x -> x + 1 x -> { return x + 1; } for single expressions
Exception handling Use custom functional interfaces for checked exceptions Wrap lambdas in try/catch making them unreadable
Reusable logic Store as named Predicate/Function variables Duplicate same lambda across multiple places

10. Complete Practical Example

Let us build an Employee Analytics System that uses lambdas throughout — every concept covered in this tutorial appears in this example. This is the kind of code you would write in real production systems when processing business data.

import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;

public class EmployeeAnalytics {

    // Simple Employee record
    static class Employee {
        String name;
        String department;
        double salary;
        int yearsOfService;

        Employee(String name, String department, double salary, int yearsOfService) {
            this.name = name;
            this.department = department;
            this.salary = salary;
            this.yearsOfService = yearsOfService;
        }

        @Override
        public String toString() {
            return String.format("%s (%s, $%.0f, %dy)", name, department, salary, yearsOfService);
        }
    }

    // Custom functional interface for business rules
    @FunctionalInterface
    interface SalaryAdjuster {
        double adjust(Employee employee);
    }

    public static void main(String[] args) {

        // ===== Setup: Create employee list =====
        List employees = new ArrayList<>(Arrays.asList(
            new Employee("Alice",   "Engineering", 95000,  5),
            new Employee("Bob",     "Engineering", 88000,  3),
            new Employee("Charlie", "Marketing",   72000,  7),
            new Employee("Diana",   "Engineering", 105000, 8),
            new Employee("Eve",     "Marketing",   68000,  2),
            new Employee("Frank",   "Sales",       78000,  4),
            new Employee("Grace",   "Sales",       82000,  6),
            new Employee("Hank",    "Marketing",   91000, 10)
        ));

        // ===== 1. Predicate: Filter employees =====
        Predicate isSenior = emp -> emp.yearsOfService >= 5;
        Predicate isHighEarner = emp -> emp.salary > 85000;
        Predicate isEngineering = emp -> emp.department.equals("Engineering");

        // Composed predicate: senior engineers earning > 85k
        Predicate seniorHighEarningEngineers =
            isSenior.and(isHighEarner).and(isEngineering);

        List elite = employees.stream()
                .filter(seniorHighEarningEngineers)
                .collect(Collectors.toList());
        System.out.println("=== Senior high-earning engineers ===");
        elite.forEach(System.out::println);
        // Output: Alice (Engineering, $95000, 5y)
        // Output: Diana (Engineering, $105000, 8y)

        // ===== 2. Function: Transform employees =====
        Function toSummary = emp ->
            emp.name + " - " + emp.department;

        List summaries = employees.stream()
                .map(toSummary)
                .collect(Collectors.toList());
        System.out.println("\n=== Employee summaries ===");
        summaries.forEach(System.out::println);
        // Output: Alice - Engineering
        // Output: Bob - Engineering
        // Output: Charlie - Marketing
        // ... (all 8)

        // ===== 3. Consumer: Process employees =====
        Consumer printBonus = emp -> {
            double bonus = emp.salary * 0.10;
            System.out.printf("  %s gets $%.0f bonus%n", emp.name, bonus);
        };

        System.out.println("\n=== Year-end bonuses (senior employees) ===");
        employees.stream()
                .filter(isSenior)
                .forEach(printBonus);
        // Output:   Alice gets $9500 bonus
        // Output:   Charlie gets $7200 bonus
        // Output:   Diana gets $10500 bonus
        // Output:   Grace gets $8200 bonus
        // Output:   Hank gets $9100 bonus

        // ===== 4. Supplier: Factory and defaults =====
        Supplier defaultEmployee = () ->
            new Employee("New Hire", "Unassigned", 50000, 0);

        Employee newHire = defaultEmployee.get();
        System.out.println("\n=== Default employee ===");
        System.out.println(newHire);
        // Output: New Hire (Unassigned, $50000, 0y)

        // ===== 5. BinaryOperator: Reduce =====
        BinaryOperator sum = Double::sum;
        double totalSalary = employees.stream()
                .map(emp -> emp.salary)
                .reduce(0.0, sum);
        System.out.printf("%n=== Total salary: $%.0f ===%n", totalSalary);
        // Output: === Total salary: $679000 ===

        // ===== 6. Comparator with lambdas =====
        employees.sort(Comparator.comparingDouble((Employee emp) -> emp.salary).reversed());
        System.out.println("\n=== Employees by salary (highest first) ===");
        employees.forEach(emp -> System.out.printf("  %-10s $%.0f%n", emp.name, emp.salary));
        // Output:   Diana      $105000
        // Output:   Alice      $95000
        // Output:   Hank       $91000
        // Output:   Bob        $88000
        // Output:   Grace      $82000
        // Output:   Frank      $78000
        // Output:   Charlie    $72000
        // Output:   Eve        $68000

        // ===== 7. Map operations with lambdas =====
        Map> byDepartment = new HashMap<>();
        employees.forEach(emp ->
            byDepartment.computeIfAbsent(emp.department, k -> new ArrayList<>()).add(emp)
        );

        System.out.println("\n=== Employees by department ===");
        byDepartment.forEach((dept, emps) -> {
            System.out.println(dept + ":");
            emps.forEach(emp -> System.out.println("  - " + emp.name));
        });
        // Output: Engineering:
        // Output:   - Diana
        // Output:   - Alice
        // Output:   - Bob
        // Output: Marketing:
        // Output:   - Hank
        // Output:   - Charlie
        // Output:   - Eve
        // Output: Sales:
        // Output:   - Grace
        // Output:   - Frank

        // ===== 8. Department salary totals using merge =====
        Map deptSalaries = new HashMap<>();
        employees.forEach(emp ->
            deptSalaries.merge(emp.department, emp.salary, Double::sum)
        );

        System.out.println("\n=== Department salary totals ===");
        deptSalaries.forEach((dept, total) ->
            System.out.printf("  %-15s $%.0f%n", dept, total)
        );
        // Output:   Engineering     $288000
        // Output:   Marketing       $231000
        // Output:   Sales           $160000

        // ===== 9. Custom functional interface: salary adjustments =====
        SalaryAdjuster standardRaise = emp -> emp.salary * 1.05;
        SalaryAdjuster seniorRaise = emp -> emp.yearsOfService >= 5
            ? emp.salary * 1.10
            : emp.salary * 1.03;
        SalaryAdjuster departmentRaise = emp -> {
            switch (emp.department) {
                case "Engineering": return emp.salary * 1.08;
                case "Sales":      return emp.salary * 1.06;
                default:           return emp.salary * 1.04;
            }
        };

        System.out.println("\n=== Salary adjustments (department-based) ===");
        employees.forEach(emp -> {
            double newSalary = departmentRaise.adjust(emp);
            System.out.printf("  %-10s $%.0f -> $%.0f%n", emp.name, emp.salary, newSalary);
        });
        // Output:   Diana      $105000 -> $113400
        // Output:   Alice      $95000 -> $102600
        // Output:   Bob        $88000 -> $95040
        // Output:   Hank       $91000 -> $94640
        // Output:   Charlie    $72000 -> $74880
        // Output:   Eve        $68000 -> $70720
        // Output:   Grace      $82000 -> $86920
        // Output:   Frank      $78000 -> $82680

        // ===== 10. removeIf and replaceAll =====
        List departments = new ArrayList<>(
            Arrays.asList("Engineering", "Marketing", "Sales", "Legal", "HR")
        );
        departments.removeIf(dept ->
            !byDepartment.containsKey(dept)  // Remove departments with no employees
        );
        departments.replaceAll(dept ->
            dept + " (" + byDepartment.get(dept).size() + " people)"
        );
        System.out.println("\n=== Active departments ===");
        departments.forEach(System.out::println);
        // Output: Engineering (3 people)
        // Output: Marketing (3 people)
        // Output: Sales (2 people)
    }
}

Concepts Demonstrated

# Concept Where Used
1 Predicate (test, and, or, negate) Filtering employees by seniority, salary, department
2 Function (apply, andThen) Transforming employees to summary strings
3 Consumer (accept, andThen) Printing bonus calculations
4 Supplier (get) Factory for default employee
5 BinaryOperator (reduce) Summing total salary
6 Comparator with lambdas Sorting by salary descending
7 Map.computeIfAbsent() Grouping employees by department
8 Map.merge() Department salary totals
9 Custom functional interface SalaryAdjuster with different strategies
10 removeIf() and replaceAll() Filtering and transforming department list
11 forEach() on List and Map Iterating and printing throughout
12 Method references System.out::println, Double::sum
13 Strategy pattern Multiple SalaryAdjuster strategies
14 Effectively final Lambda captures in forEach blocks

Quick Reference

Topic Key Point
Lambda syntax (params) -> expression or (params) -> { statements; }
Type inference Compiler infers param types from context (target typing)
Effectively final Local variables captured by lambdas must never be reassigned
Predicate<T> T -> boolean. Composable with and(), or(), negate()
Function<T,R> T -> R. Chain with andThen(), compose()
Consumer<T> T -> void. Side effects. Chain with andThen()
Supplier<T> () -> T. Factories, lazy defaults
BiFunction<T,U,R> (T, U) -> R. Two inputs, one output
UnaryOperator<T> T -> T. Same-type transformation. Used by replaceAll()
BinaryOperator<T> (T, T) -> T. Used by reduce(), merge()
this in lambdas Refers to the enclosing class (not the lambda itself)
Lambda vs anonymous class Lambda: invokedynamic, no class file, shares enclosing scope
forEach() Iterable method. Takes Consumer. Use for simple iteration
removeIf() Collection method. Takes Predicate. Removes matching elements
replaceAll() List method. Takes UnaryOperator. Transforms every element
computeIfAbsent() Map method. Initializes missing keys with lambda factory
merge() Map method. Combines old and new values with BiFunction
Best practice Keep short, use method references, avoid side effects, name params
August 5, 2019

Iterator Pattern

Introduction

The Iterator Pattern provides a way to access elements of a collection sequentially without exposing its underlying representation. Whether the data lives in an array, a linked list, a tree, or a paginated API, the iterator gives clients a uniform interface to traverse it: hasNext() and next().

This pattern separates the traversal logic from the collection itself, allowing multiple traversal strategies over the same data structure without modifying the collection class.

The Problem

You are building a service that fetches results from a paginated REST API. The API returns 20 items per page, and you need to process all items across all pages. The naive approach is to embed the pagination logic directly in your business code — tracking page numbers, checking for the last page, and concatenating results.

Now imagine another part of your codebase also needs to iterate over the same API but with different filtering. You end up duplicating pagination logic everywhere. Worse, if the API changes its pagination scheme (from page-based to cursor-based), you must update every call site.

The Solution

The Iterator Pattern encapsulates the pagination logic inside an iterator object. Clients simply call hasNext() and next() without knowing whether the data comes from one page or fifty. The iterator handles fetching the next page transparently.

This means your business code stays clean and focused on processing items, while the iterator handles the mechanics of traversal and data fetching.

Key Principle

Single Responsibility Principle. The collection is responsible for storing data. The iterator is responsible for traversing it. Neither takes on the other’s job, making both easier to maintain and extend.

Java Example

Scenario: An iterator that transparently traverses paginated API results.

import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;

// Simulated API response
public class Page<T> {
    private final List<T> items;
    private final int currentPage;
    private final int totalPages;

    public Page(List<T> items, int currentPage, int totalPages) {
        this.items = items;
        this.currentPage = currentPage;
        this.totalPages = totalPages;
    }

    public List<T> getItems() { return items; }
    public int getCurrentPage() { return currentPage; }
    public int getTotalPages() { return totalPages; }
    public boolean hasNextPage() { return currentPage < totalPages; }
}

// Simulated API client
public class ProductApiClient {
    private final List<List<String>> pages = List.of(
        List.of("Laptop", "Keyboard", "Mouse"),
        List.of("Monitor", "Headphones", "Webcam"),
        List.of("USB Hub", "Desk Lamp")
    );

    public Page<String> fetchPage(int pageNumber) {
        System.out.println("  [API] Fetching page " + pageNumber + "...");
        int index = pageNumber - 1;
        return new Page<>(pages.get(index), pageNumber, pages.size());
    }
}

// Iterator for paginated results
public class PaginatedIterator<T> implements Iterator<T> {
    private final ProductApiClient apiClient;
    private Page<String> currentPage;
    private int itemIndex = 0;

    public PaginatedIterator(ProductApiClient apiClient) {
        this.apiClient = apiClient;
        this.currentPage = apiClient.fetchPage(1);
    }

    @Override
    public boolean hasNext() {
        if (itemIndex < currentPage.getItems().size()) {
            return true;
        }
        // Current page exhausted — check if more pages exist
        return currentPage.hasNextPage();
    }

    @Override
    @SuppressWarnings("unchecked")
    public T next() {
        // If current page is exhausted, fetch the next one
        if (itemIndex >= currentPage.getItems().size()) {
            if (!currentPage.hasNextPage()) {
                throw new NoSuchElementException("No more items");
            }
            currentPage = apiClient.fetchPage(
                currentPage.getCurrentPage() + 1);
            itemIndex = 0;
        }
        return (T) currentPage.getItems().get(itemIndex++);
    }
}

// Iterable collection that provides the iterator
public class ProductCatalog implements Iterable<String> {
    private final ProductApiClient apiClient;

    public ProductCatalog(ProductApiClient apiClient) {
        this.apiClient = apiClient;
    }

    @Override
    public Iterator<String> iterator() {
        return new PaginatedIterator<>(apiClient);
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        ProductApiClient api = new ProductApiClient();
        ProductCatalog catalog = new ProductCatalog(api);

        System.out.println("Iterating over all products:\n");

        // Clean for-each loop — pagination is invisible
        int count = 1;
        for (String product : catalog) {
            System.out.println("  " + count++ + ". " + product);
        }

        System.out.println("\nProcessed all products across all pages.");
    }
}

Python Example

Same paginated API traversal in Python using the iterator protocol.

from dataclasses import dataclass


@dataclass
class Page:
    items: list[str]
    current_page: int
    total_pages: int

    @property
    def has_next_page(self) -> bool:
        return self.current_page < self.total_pages


# Simulated API client
class ProductApiClient:
    def __init__(self):
        self._pages = [
            ["Laptop", "Keyboard", "Mouse"],
            ["Monitor", "Headphones", "Webcam"],
            ["USB Hub", "Desk Lamp"],
        ]

    def fetch_page(self, page_number: int) -> Page:
        print(f"  [API] Fetching page {page_number}...")
        items = self._pages[page_number - 1]
        return Page(items, page_number, len(self._pages))


# Iterator for paginated results
class PaginatedIterator:
    def __init__(self, api_client: ProductApiClient):
        self._api_client = api_client
        self._current_page = api_client.fetch_page(1)
        self._item_index = 0

    def __iter__(self):
        return self

    def __next__(self) -> str:
        # If current page is exhausted, fetch the next one
        if self._item_index >= len(self._current_page.items):
            if not self._current_page.has_next_page:
                raise StopIteration
            next_page_num = self._current_page.current_page + 1
            self._current_page = self._api_client.fetch_page(next_page_num)
            self._item_index = 0

        item = self._current_page.items[self._item_index]
        self._item_index += 1
        return item


# Iterable collection that provides the iterator
class ProductCatalog:
    def __init__(self, api_client: ProductApiClient):
        self._api_client = api_client

    def __iter__(self) -> PaginatedIterator:
        return PaginatedIterator(self._api_client)


# Usage
if __name__ == "__main__":
    api = ProductApiClient()
    catalog = ProductCatalog(api)

    print("Iterating over all products:\n")

    # Clean for loop — pagination is invisible
    for count, product in enumerate(catalog, start=1):
        print(f"  {count}. {product}")

    print("\nProcessed all products across all pages.")

When to Use

  • Paginated data sources — When you need to traverse results from APIs, databases, or file systems that return data in chunks.
  • Hiding collection internals — When clients should not know whether the underlying data structure is a list, tree, hash map, or external service.
  • Multiple traversal strategies — When you need different ways to traverse the same collection (forward, reverse, filtered, depth-first, breadth-first).
  • Lazy evaluation — When loading all data upfront is expensive or impossible, and you want to fetch items on demand as the client iterates.

Real-World Usage

  • Java Iterator and Iterable — The foundation of Java’s for-each loop. Every collection in java.util implements Iterable.
  • Python Iterators and Generators — Python’s for loop uses the iterator protocol (__iter__ / __next__). Generators with yield are a concise way to create iterators.
  • JDBC ResultSet — Iterates over database query results row by row without loading the entire result set into memory.
  • Spring Data Page / Slice — Spring Data’s pagination abstractions use the iterator pattern to traverse large datasets from repositories.
August 4, 2019

State Pattern

Introduction

The State Pattern allows an object to alter its behavior when its internal state changes, making it appear as though the object has changed its class. Instead of scattering state-dependent logic across methods with conditionals, you encapsulate each state into its own class, and the object delegates behavior to its current state.

If you have ever seen a method riddled with if (status == "pending") checks scattered throughout a class, the State Pattern is the clean alternative.

The Problem

You are building an order management system. An order moves through several states: Pending, Processing, Shipped, and Delivered. Each state allows different actions — you can cancel a pending order but not a shipped one. You can ship a processing order but not a delivered one.

The naive approach is to add status checks in every method: cancel() checks if status is “pending,” ship() checks if status is “processing,” and so on. As states and transitions grow, the class becomes a tangled web of conditionals. Adding a new state (like “Returned”) means touching every method.

The Solution

The State Pattern creates a separate class for each state. Each state class implements the same interface and defines what actions are valid in that state. The order object (context) holds a reference to its current state and delegates all actions to it. State transitions happen by swapping the current state object.

This eliminates conditional logic and makes each state’s behavior self-contained. Adding a new state means adding a new class — no existing state classes need to change.

Key Principle

Encapsulate what varies. The thing that varies here is the behavior associated with each state. By extracting each state into its own class, you isolate state-specific logic and make transitions explicit and traceable.

Java Example

Scenario: An order management system with state-driven behavior.

// State interface
public interface OrderState {
    void next(OrderContext order);
    void cancel(OrderContext order);
    String getStatus();
}

// Context
public class OrderContext {
    private OrderState state;
    private final String orderId;

    public OrderContext(String orderId) {
        this.orderId = orderId;
        this.state = new PendingState();
        System.out.println("Order " + orderId + " created. Status: " + state.getStatus());
    }

    public void setState(OrderState state) {
        this.state = state;
        System.out.println("Order " + orderId + " status: " + state.getStatus());
    }

    public void next() {
        state.next(this);
    }

    public void cancel() {
        state.cancel(this);
    }

    public String getStatus() {
        return state.getStatus();
    }

    public String getOrderId() {
        return orderId;
    }
}

// Concrete State: Pending
public class PendingState implements OrderState {
    @Override
    public void next(OrderContext order) {
        System.out.println("Processing payment for order " + order.getOrderId() + "...");
        order.setState(new ProcessingState());
    }

    @Override
    public void cancel(OrderContext order) {
        System.out.println("Order " + order.getOrderId() + " cancelled.");
        order.setState(new CancelledState());
    }

    @Override
    public String getStatus() {
        return "PENDING";
    }
}

// Concrete State: Processing
public class ProcessingState implements OrderState {
    @Override
    public void next(OrderContext order) {
        System.out.println("Order " + order.getOrderId() + " shipped!");
        order.setState(new ShippedState());
    }

    @Override
    public void cancel(OrderContext order) {
        System.out.println("Order " + order.getOrderId()
            + " is being processed. Initiating cancellation...");
        order.setState(new CancelledState());
    }

    @Override
    public String getStatus() {
        return "PROCESSING";
    }
}

// Concrete State: Shipped
public class ShippedState implements OrderState {
    @Override
    public void next(OrderContext order) {
        System.out.println("Order " + order.getOrderId() + " delivered.");
        order.setState(new DeliveredState());
    }

    @Override
    public void cancel(OrderContext order) {
        System.out.println("Cannot cancel order " + order.getOrderId()
            + " — already shipped.");
    }

    @Override
    public String getStatus() {
        return "SHIPPED";
    }
}

// Concrete State: Delivered
public class DeliveredState implements OrderState {
    @Override
    public void next(OrderContext order) {
        System.out.println("Order " + order.getOrderId()
            + " already delivered. No further transitions.");
    }

    @Override
    public void cancel(OrderContext order) {
        System.out.println("Cannot cancel order " + order.getOrderId()
            + " — already delivered.");
    }

    @Override
    public String getStatus() {
        return "DELIVERED";
    }
}

// Concrete State: Cancelled
public class CancelledState implements OrderState {
    @Override
    public void next(OrderContext order) {
        System.out.println("Order " + order.getOrderId()
            + " is cancelled. Cannot proceed.");
    }

    @Override
    public void cancel(OrderContext order) {
        System.out.println("Order " + order.getOrderId()
            + " is already cancelled.");
    }

    @Override
    public String getStatus() {
        return "CANCELLED";
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        OrderContext order = new OrderContext("ORD-2001");

        order.next();      // Pending -> Processing
        order.next();      // Processing -> Shipped
        order.cancel();    // Cannot cancel — already shipped
        order.next();      // Shipped -> Delivered
        order.next();      // Already delivered

        System.out.println("\n--- Cancellation flow ---");
        OrderContext order2 = new OrderContext("ORD-2002");
        order2.cancel();   // Pending -> Cancelled
        order2.next();     // Cannot proceed — cancelled
    }
}

Python Example

Same order management system in Python.

from abc import ABC, abstractmethod


# State interface
class OrderState(ABC):
    @abstractmethod
    def next(self, order: "OrderContext") -> None:
        pass

    @abstractmethod
    def cancel(self, order: "OrderContext") -> None:
        pass

    @abstractmethod
    def get_status(self) -> str:
        pass


# Context
class OrderContext:
    def __init__(self, order_id: str):
        self.order_id = order_id
        self._state: OrderState = PendingState()
        print(f"Order {order_id} created. Status: {self._state.get_status()}")

    def set_state(self, state: OrderState) -> None:
        self._state = state
        print(f"Order {self.order_id} status: {self._state.get_status()}")

    def next(self) -> None:
        self._state.next(self)

    def cancel(self) -> None:
        self._state.cancel(self)

    @property
    def status(self) -> str:
        return self._state.get_status()


# Concrete State: Pending
class PendingState(OrderState):
    def next(self, order: OrderContext) -> None:
        print(f"Processing payment for order {order.order_id}...")
        order.set_state(ProcessingState())

    def cancel(self, order: OrderContext) -> None:
        print(f"Order {order.order_id} cancelled.")
        order.set_state(CancelledState())

    def get_status(self) -> str:
        return "PENDING"


# Concrete State: Processing
class ProcessingState(OrderState):
    def next(self, order: OrderContext) -> None:
        print(f"Order {order.order_id} shipped!")
        order.set_state(ShippedState())

    def cancel(self, order: OrderContext) -> None:
        print(f"Order {order.order_id} is being processed. "
              f"Initiating cancellation...")
        order.set_state(CancelledState())

    def get_status(self) -> str:
        return "PROCESSING"


# Concrete State: Shipped
class ShippedState(OrderState):
    def next(self, order: OrderContext) -> None:
        print(f"Order {order.order_id} delivered.")
        order.set_state(DeliveredState())

    def cancel(self, order: OrderContext) -> None:
        print(f"Cannot cancel order {order.order_id} — already shipped.")

    def get_status(self) -> str:
        return "SHIPPED"


# Concrete State: Delivered
class DeliveredState(OrderState):
    def next(self, order: OrderContext) -> None:
        print(f"Order {order.order_id} already delivered. "
              f"No further transitions.")

    def cancel(self, order: OrderContext) -> None:
        print(f"Cannot cancel order {order.order_id} — already delivered.")

    def get_status(self) -> str:
        return "DELIVERED"


# Concrete State: Cancelled
class CancelledState(OrderState):
    def next(self, order: OrderContext) -> None:
        print(f"Order {order.order_id} is cancelled. Cannot proceed.")

    def cancel(self, order: OrderContext) -> None:
        print(f"Order {order.order_id} is already cancelled.")

    def get_status(self) -> str:
        return "CANCELLED"


# Usage
if __name__ == "__main__":
    order = OrderContext("ORD-2001")

    order.next()      # Pending -> Processing
    order.next()      # Processing -> Shipped
    order.cancel()    # Cannot cancel — already shipped
    order.next()      # Shipped -> Delivered
    order.next()      # Already delivered

    print("\n--- Cancellation flow ---")
    order2 = OrderContext("ORD-2002")
    order2.cancel()   # Pending -> Cancelled
    order2.next()     # Cannot proceed — cancelled

When to Use

  • Objects with distinct behavioral states — When an object behaves differently depending on its current state, and transitions between states are well-defined (orders, workflows, game characters).
  • Eliminating state conditionals — When methods are cluttered with if/else or switch statements that check the current state before acting.
  • State machines — When modeling finite state machines where transitions and allowed actions are state-dependent.
  • Workflow engines — When documents, tickets, or requests move through approval stages with different rules at each stage.

Real-World Usage

  • TCP Connection — A TCP socket transitions through states (Listening, Established, Closed) with different behaviors in each.
  • Java Thread States — The Thread class has states (NEW, RUNNABLE, BLOCKED, TERMINATED) that determine allowed operations.
  • Spring State Machine — The spring-statemachine project provides a framework for building state-based applications using this pattern.
  • Workflow tools (Jira, GitHub Issues) — Issue status transitions (Open, In Progress, Done) follow the state pattern with rules about valid transitions.
August 4, 2019

Template Method Pattern

Introduction

The Template Method Pattern defines the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the algorithm’s overall structure. The base class controls the workflow — the “what” and “when” — while subclasses provide the “how” for individual steps.

This pattern is everywhere in frameworks. Every time you extend a base class and override a hook method (like setUp() in a test framework or doGet() in a servlet), you are using the Template Method Pattern.

The Problem

You are building a data export system. Users can export records as CSV, JSON, or XML. Each export follows the same high-level workflow: fetch data, validate it, transform it into the target format, and write the output. The naive approach is to implement three separate export classes, each duplicating the fetch and validation logic.

This leads to code duplication. When the validation rules change, you must update three classes. When you add a new export format, you copy-paste an existing class and modify the format-specific parts — a recipe for bugs and inconsistency.

The Solution

The Template Method Pattern pulls the shared workflow into a base class method (the “template method”) that calls abstract steps for the parts that vary. The base class handles fetching and validation, while subclasses implement only the format-specific transformation and writing logic.

The template method is typically marked as final (in Java) to prevent subclasses from altering the workflow order. Subclasses can customize individual steps but cannot skip validation or reorder the pipeline.

Key Principle

The Hollywood Principle: “Don’t call us, we’ll call you.” The base class controls the flow and calls subclass methods at the right time. Subclasses do not drive the algorithm — they fill in the blanks.

Java Example

Scenario: A data export pipeline supporting CSV, JSON, and XML formats.

import java.util.List;
import java.util.Map;

// Abstract base class with template method
public abstract class DataExporter {

    // Template method — defines the algorithm skeleton
    public final void export(List<Map<String, String>> records) {
        validate(records);
        String formatted = transform(records);
        String fileName = getFileName();
        writeOutput(fileName, formatted);
        System.out.println("Export complete: " + fileName + "\n");
    }

    // Shared step — same for all formats
    private void validate(List<Map<String, String>> records) {
        if (records == null || records.isEmpty()) {
            throw new IllegalArgumentException("No records to export");
        }
        System.out.println("Validated " + records.size() + " records.");
    }

    // Abstract steps — subclasses provide the implementation
    protected abstract String transform(List<Map<String, String>> records);
    protected abstract String getFileName();

    // Hook method — default implementation, can be overridden
    protected void writeOutput(String fileName, String content) {
        System.out.println("Writing to " + fileName
            + " (" + content.length() + " chars)");
    }
}

// Concrete: CSV Exporter
public class CsvExporter extends DataExporter {

    @Override
    protected String transform(List<Map<String, String>> records) {
        StringBuilder sb = new StringBuilder();
        // Header row
        sb.append(String.join(",", records.get(0).keySet())).append("\n");
        // Data rows
        for (Map<String, String> record : records) {
            sb.append(String.join(",", record.values())).append("\n");
        }
        return sb.toString();
    }

    @Override
    protected String getFileName() {
        return "export.csv";
    }
}

// Concrete: JSON Exporter
public class JsonExporter extends DataExporter {

    @Override
    protected String transform(List<Map<String, String>> records) {
        StringBuilder sb = new StringBuilder("[\n");
        for (int i = 0; i < records.size(); i++) {
            sb.append("  {");
            Map<String, String> record = records.get(i);
            int j = 0;
            for (Map.Entry<String, String> entry : record.entrySet()) {
                sb.append("\"").append(entry.getKey()).append("\": \"")
                  .append(entry.getValue()).append("\"");
                if (++j < record.size()) sb.append(", ");
            }
            sb.append("}");
            if (i < records.size() - 1) sb.append(",");
            sb.append("\n");
        }
        sb.append("]");
        return sb.toString();
    }

    @Override
    protected String getFileName() {
        return "export.json";
    }
}

// Concrete: XML Exporter
public class XmlExporter extends DataExporter {

    @Override
    protected String transform(List<Map<String, String>> records) {
        StringBuilder sb = new StringBuilder("<records>\n");
        for (Map<String, String> record : records) {
            sb.append("  <record>\n");
            for (Map.Entry<String, String> entry : record.entrySet()) {
                sb.append("    <").append(entry.getKey()).append(">")
                  .append(entry.getValue())
                  .append("</").append(entry.getKey()).append(">\n");
            }
            sb.append("  </record>\n");
        }
        sb.append("</records>");
        return sb.toString();
    }

    @Override
    protected String getFileName() {
        return "export.xml";
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        List<Map<String, String>> records = List.of(
            Map.of("name", "Alice", "role", "Engineer"),
            Map.of("name", "Bob", "role", "Designer")
        );

        new CsvExporter().export(records);
        new JsonExporter().export(records);
        new XmlExporter().export(records);
    }
}

Python Example

Same data export pipeline in Python.

from abc import ABC, abstractmethod


class DataExporter(ABC):
    """Abstract base class with template method."""

    def export(self, records: list[dict[str, str]]) -> None:
        """Template method — defines the algorithm skeleton."""
        self._validate(records)
        formatted = self.transform(records)
        file_name = self.get_file_name()
        self.write_output(file_name, formatted)
        print(f"Export complete: {file_name}\n")

    def _validate(self, records: list[dict[str, str]]) -> None:
        """Shared step — same for all formats."""
        if not records:
            raise ValueError("No records to export")
        print(f"Validated {len(records)} records.")

    @abstractmethod
    def transform(self, records: list[dict[str, str]]) -> str:
        """Subclasses provide format-specific transformation."""
        pass

    @abstractmethod
    def get_file_name(self) -> str:
        pass

    def write_output(self, file_name: str, content: str) -> None:
        """Hook method — default implementation, can be overridden."""
        print(f"Writing to {file_name} ({len(content)} chars)")


class CsvExporter(DataExporter):

    def transform(self, records: list[dict[str, str]]) -> str:
        headers = ",".join(records[0].keys())
        rows = [",".join(record.values()) for record in records]
        return headers + "\n" + "\n".join(rows)

    def get_file_name(self) -> str:
        return "export.csv"


class JsonExporter(DataExporter):

    def transform(self, records: list[dict[str, str]]) -> str:
        import json
        return json.dumps(records, indent=2)

    def get_file_name(self) -> str:
        return "export.json"


class XmlExporter(DataExporter):

    def transform(self, records: list[dict[str, str]]) -> str:
        lines = ["<records>"]
        for record in records:
            lines.append("  <record>")
            for key, value in record.items():
                lines.append(f"    <{key}>{value}</{key}>")
            lines.append("  </record>")
        lines.append("</records>")
        return "\n".join(lines)

    def get_file_name(self) -> str:
        return "export.xml"


# Usage
if __name__ == "__main__":
    records = [
        {"name": "Alice", "role": "Engineer"},
        {"name": "Bob", "role": "Designer"},
    ]

    CsvExporter().export(records)
    JsonExporter().export(records)
    XmlExporter().export(records)

When to Use

  • Shared workflow, varying steps — When multiple classes follow the same algorithm but differ in specific steps (e.g., different export formats, different report generators).
  • Enforcing a sequence — When you want to guarantee that steps execute in a specific order (validation before transformation, setup before execution).
  • Reducing code duplication — When subclasses share significant logic and only a few steps differ between them.
  • Framework extension points — When building a framework where users override hooks without altering the core workflow.

Real-World Usage

  • Java Servlet HttpServlet — The service() method is the template that dispatches to doGet(), doPost(), etc.
  • JUnit / TestNG — The test lifecycle (setUp(), test method, tearDown()) is a template method pattern.
  • Spring JdbcTemplate — Handles connection management and error handling while letting you provide the query-specific logic.
  • Python unittest.TestCase — The setUp() / test_*() / tearDown() lifecycle follows the template method pattern.
August 4, 2019

Observer Pattern

Introduction

The Observer Pattern establishes a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. Think of it as a subscription model — interested parties register to receive updates, and the publisher broadcasts changes without knowing who is listening.

This pattern is the backbone of event-driven architectures. From GUI button clicks to microservice event buses, the Observer Pattern enables loose coupling between components that need to react to changes.

The Problem

You are building an e-commerce platform. When an order is placed, several things must happen: send a confirmation email, update inventory, notify the analytics service, and trigger the shipping workflow. The naive approach is to put all this logic directly in the placeOrder() method.

This creates tight coupling — the order service now depends on the email service, inventory service, analytics service, and shipping service. Every time you add a new reaction to “order placed,” you must modify the order class. Testing becomes painful because you cannot place an order without triggering every downstream system.

The Solution

The Observer Pattern decouples the event source (order service) from the event handlers (email, inventory, analytics). The order service maintains a list of observers and simply notifies them when an order is placed. Each observer decides independently how to react.

Adding a new reaction — say, a loyalty points service — requires only creating a new observer and registering it. The order service code remains untouched.

Key Principle

Strive for loosely coupled designs between objects that interact. The subject (publisher) knows nothing about its observers except that they implement a common interface. This means you can add, remove, or replace observers without modifying the subject.

Java Example

Scenario: An order event system that notifies multiple services when an order is placed.

import java.util.ArrayList;
import java.util.List;

// Event data
public class OrderEvent {
    private final String orderId;
    private final String customerEmail;
    private final double totalAmount;
    private final List<String> items;

    public OrderEvent(String orderId, String customerEmail,
                      double totalAmount, List<String> items) {
        this.orderId = orderId;
        this.customerEmail = customerEmail;
        this.totalAmount = totalAmount;
        this.items = items;
    }

    public String getOrderId() { return orderId; }
    public String getCustomerEmail() { return customerEmail; }
    public double getTotalAmount() { return totalAmount; }
    public List<String> getItems() { return items; }
}

// Observer interface
public interface OrderObserver {
    void onOrderPlaced(OrderEvent event);
}

// Subject (Publisher)
public class OrderService {
    private final List<OrderObserver> observers = new ArrayList<>();

    public void subscribe(OrderObserver observer) {
        observers.add(observer);
    }

    public void unsubscribe(OrderObserver observer) {
        observers.remove(observer);
    }

    public void placeOrder(String orderId, String email,
                           double total, List<String> items) {
        System.out.println("Order " + orderId + " placed successfully.");
        OrderEvent event = new OrderEvent(orderId, email, total, items);

        // Notify all observers
        for (OrderObserver observer : observers) {
            observer.onOrderPlaced(event);
        }
    }
}

// Concrete Observer: Email Notification
public class EmailNotificationObserver implements OrderObserver {
    @Override
    public void onOrderPlaced(OrderEvent event) {
        System.out.printf("[Email] Sending confirmation to %s for order %s ($%.2f)%n",
            event.getCustomerEmail(), event.getOrderId(), event.getTotalAmount());
    }
}

// Concrete Observer: Inventory Update
public class InventoryObserver implements OrderObserver {
    @Override
    public void onOrderPlaced(OrderEvent event) {
        System.out.printf("[Inventory] Reserving %d items for order %s%n",
            event.getItems().size(), event.getOrderId());
    }
}

// Concrete Observer: Analytics Tracking
public class AnalyticsObserver implements OrderObserver {
    @Override
    public void onOrderPlaced(OrderEvent event) {
        System.out.printf("[Analytics] Tracking order %s — revenue: $%.2f%n",
            event.getOrderId(), event.getTotalAmount());
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        OrderService orderService = new OrderService();

        // Register observers
        orderService.subscribe(new EmailNotificationObserver());
        orderService.subscribe(new InventoryObserver());
        orderService.subscribe(new AnalyticsObserver());

        // Place an order — all observers get notified
        orderService.placeOrder("ORD-1001", "jane@example.com", 149.99,
            List.of("Laptop Stand", "USB-C Hub"));
    }
}

Python Example

Same order notification system in Python.

from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass
class OrderEvent:
    order_id: str
    customer_email: str
    total_amount: float
    items: list[str]


# Observer interface
class OrderObserver(ABC):
    @abstractmethod
    def on_order_placed(self, event: OrderEvent) -> None:
        pass


# Subject (Publisher)
class OrderService:
    def __init__(self):
        self._observers: list[OrderObserver] = []

    def subscribe(self, observer: OrderObserver) -> None:
        self._observers.append(observer)

    def unsubscribe(self, observer: OrderObserver) -> None:
        self._observers.remove(observer)

    def place_order(self, order_id: str, email: str,
                    total: float, items: list[str]) -> None:
        print(f"Order {order_id} placed successfully.")
        event = OrderEvent(order_id, email, total, items)

        # Notify all observers
        for observer in self._observers:
            observer.on_order_placed(event)


# Concrete Observer: Email Notification
class EmailNotificationObserver(OrderObserver):
    def on_order_placed(self, event: OrderEvent) -> None:
        print(f"[Email] Sending confirmation to {event.customer_email} "
              f"for order {event.order_id} (${event.total_amount:.2f})")


# Concrete Observer: Inventory Update
class InventoryObserver(OrderObserver):
    def on_order_placed(self, event: OrderEvent) -> None:
        print(f"[Inventory] Reserving {len(event.items)} items "
              f"for order {event.order_id}")


# Concrete Observer: Analytics Tracking
class AnalyticsObserver(OrderObserver):
    def on_order_placed(self, event: OrderEvent) -> None:
        print(f"[Analytics] Tracking order {event.order_id} "
              f"— revenue: ${event.total_amount:.2f}")


# Usage
if __name__ == "__main__":
    order_service = OrderService()

    # Register observers
    order_service.subscribe(EmailNotificationObserver())
    order_service.subscribe(InventoryObserver())
    order_service.subscribe(AnalyticsObserver())

    # Place an order — all observers get notified
    order_service.place_order(
        "ORD-1001", "jane@example.com", 149.99,
        ["Laptop Stand", "USB-C Hub"]
    )

When to Use

  • Event-driven systems — When one action should trigger multiple independent reactions (e.g., order placed, user signed up, file uploaded).
  • Decoupling modules — When the publisher should not know or care about who reacts to its events.
  • Dynamic subscriptions — When observers need to be added or removed at runtime (e.g., enabling/disabling notification channels).
  • Cross-cutting concerns — When logging, auditing, or metrics need to react to business events without polluting core logic.

Real-World Usage

  • Java Swing/AWT Event Listeners — GUI components use observers (ActionListener, MouseListener) to handle user interactions.
  • Spring ApplicationEvent — Spring’s event system lets beans publish and listen to application events using the observer pattern.
  • JavaScript DOM EventsaddEventListener() is the observer pattern — elements are subjects, handlers are observers.
  • Apache Kafka / RabbitMQ — Message broker pub/sub systems are distributed implementations of the observer pattern.
August 4, 2019