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:
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.
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.
| 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
}
}
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]
}
}
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
}
}
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:
Object (toString, equals, hashCode) — these do not count@FunctionalInterface is optional but recommended for compile-time safetyJava 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 |
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
}
}
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]
}
}
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
}
}
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
}
}
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)
}
}
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
}
}
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
}
}
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.
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
}
}
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]
}
}
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}
}
}
Lambdas have access to variables from the enclosing scope. Understanding exactly what they can and cannot access is critical for writing correct lambda code.
| 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();
}
}
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();
}
}
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,
}
}
Lambdas and anonymous classes both create implementations of interfaces on the fly. However, they differ in important ways beyond just syntax.
| 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) |
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
}
}
Lambdas cannot replace anonymous classes in every situation. Use an anonymous class when:
this to refer to the anonymous instance itselfLambdas 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
}
}
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]
}
}
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
}
}
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 @]
}
}
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
}
}
Writing good lambda code requires discipline. These best practices come from real-world Java 8+ production codebases.
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]
}
}
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]
}
}
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
}
}
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]
}
}
| 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 |
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)
}
}
| # | 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 |
| 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 |
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.
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 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.
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.
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.");
}
}
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.")
Iterator and Iterable — The foundation of Java’s for-each loop. Every collection in java.util implements Iterable.for loop uses the iterator protocol (__iter__ / __next__). Generators with yield are a concise way to create iterators.ResultSet — Iterates over database query results row by row without loading the entire result set into memory.Page / Slice — Spring Data’s pagination abstractions use the iterator pattern to traverse large datasets from repositories.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.
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 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.
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.
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
}
}
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
if/else or switch statements that check the current state before acting.Thread class has states (NEW, RUNNABLE, BLOCKED, TERMINATED) that determine allowed operations.spring-statemachine project provides a framework for building state-based applications using this pattern.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.
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 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.
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.
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);
}
}
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)
HttpServlet — The service() method is the template that dispatches to doGet(), doPost(), etc.setUp(), test method, tearDown()) is a template method pattern.JdbcTemplate — Handles connection management and error handling while letting you provide the query-specific logic.unittest.TestCase — The setUp() / test_*() / tearDown() lifecycle follows the template method pattern.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.
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 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.
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.
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"));
}
}
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"]
)
ActionListener, MouseListener) to handle user interactions.ApplicationEvent — Spring’s event system lets beans publish and listen to application events using the observer pattern.addEventListener() is the observer pattern — elements are subjects, handlers are observers.