skip to content

How have functional interfaces and lambdas reshaped how classic patterns like Strategy are written in modern Java?

level: seniorimportance: should knowfreq 55%

answer

  1. Functional interface = one abstract method (SAM); lambda implements it
  2. Comparator/Runnable/Callable + java.util.function are all SAMs
  3. Strategy/Command collapse from a class to a lambda
  4. default methods compose: Comparator.comparing().thenComparing()
  5. Named class still wins for state, reuse, stack-trace names, multi-method

basics

~20 s

A functional interface has one abstract method, so you can pass a lambda where it's expected. That turns patterns like Strategy into a one-line lambda instead of a whole class — e.g. passing a Comparator to sort, or a Runnable to a thread.

solid answer

~50 s

Before Java 8, behavioural patterns like Strategy, Command, and Template Method's hooks needed a named class or a verbose anonymous inner class to pass behaviour around. Java 8 added lambdas and method references, which are shorthand for implementing a **functional interface** — any interface with a single abstract method (SAM), optionally marked `@FunctionalInterface`. Because `Comparator`, `Runnable`, `Callable`, and the `java.util.function` family (`Function`, `Predicate`, `Supplier`, `Consumer`) are all functional interfaces, a strategy becomes a lambda: `list.sort((a, b) -> a.age() - b.age())` instead of a `class AgeComparator`. This collapses the ceremony — Strategy and Command often no longer need a dedicated interface or class at all; you compose first-class functions. The pattern's *intent* (pluggable, runtime-selected behaviour) is unchanged, but the *implementation* is lighter, more readable, and composable (e.g. `Comparator.comparing(...).thenComparing(...)`). The trade-off: lambdas have no name in stack traces and can't easily hold state, so a named class still wins when the strategy is complex or reused.

code

java · 17 lines
java
// Pre-8 Strategy: anonymous inner class implementing the Comparator strategy
Collections.sort(users, new Comparator<User>() {
    public int compare(User a, User b) {
        return Integer.compare(a.age(), b.age());
    }
});

// Java 8+: the same strategy as a lambda / method reference, composable
users.sort(Comparator.comparingInt(User::age)
                     .thenComparing(User::name)   // compose strategies
                     .reversed());

@FunctionalInterface                 // optional: enforces the single-abstract-method rule
interface DiscountPolicy { long apply(long cents); }

DiscountPolicy tenPercent = c -> c - c / 10;   // a Strategy with no class needed
long price = tenPercent.apply(1000);           // 900

go deeper

for a junior

Knows a lambda can be passed where a single-method interface like Runnable or Comparator is expected.

for a middle

Defines functional interface/SAM, rewrites an anonymous inner class as a lambda, and uses java.util.function types.

for a senior

Explains that lambdas streamline Strategy/Command without changing their intent, uses comparator/predicate composition, and states the trade-offs vs named classes.

for a principal

Frames first-class functions as a shift toward composition over inheritance, and sets guidance on when lambdas vs named strategies serve maintainability at scale.

## Background terms - A **lambda expression** (Java 8+) is an anonymous function literal: `(a, b) -> a + b`. It's a concise way to write a chunk of behaviour inline. - A **functional interface** is an interface with exactly **one abstract method** (a 'SAM' — Single Abstract Method). `default` and `static` methods don't count against the one. The optional `@FunctionalInterface` annotation makes the compiler enforce the single-abstract-method rule. Examples: `Runnable` (`run`), `Comparator` (`compare`), `Callable` (`call`), and the `java.util.function` package: `Function<T,R>`, `Predicate<T>`, `Supplier<T>`, `Consumer<T>`, `BiFunction`, etc. - A lambda is just a compact way to provide an instance of a functional interface. `Runnable r = () -> System.out.println("hi");` — the lambda's body becomes `run()`. The compiler infers which functional interface from the target type (the 'target typing' rule). - A **method reference** (`User::getName`, `System.out::println`) is even shorter syntax for a lambda that just calls an existing method. ## The Strategy pattern, before and after The **Strategy pattern** encapsulates an interchangeable algorithm behind a common interface, so callers can pick the algorithm at runtime. Classic structure: a `Strategy` interface, several concrete implementing classes, and a context that holds one and delegates to it. **Pre-Java-8** you supplied a strategy with either a named class or an **anonymous inner class** — verbose: ```java Collections.sort(list, new Comparator<User>() { public int compare(User a, User b) { return a.getAge() - b.getAge(); } }); ``` That's ~4 lines of boilerplate around one line of logic, plus a class with no real identity. **Post-Java-8**, because `Comparator` is a functional interface, the strategy is a lambda: ```java list.sort((a, b) -> a.getAge() - b.getAge()); // or, clearer: list.sort(Comparator.comparingInt(User::getAge)); ``` The `Comparator` *interface* still exists — that's the strategy abstraction — but you no longer write a class to implement it. The pattern's intent (pluggable, runtime-selected ordering) is identical; only the boilerplate is gone. ## Which patterns this affects Any pattern whose participant is essentially 'a piece of behaviour' collapses to a lambda/functional interface: - **Strategy** → pass a `Function`/`Comparator`/custom SAM. - **Command** → a `Runnable`/`Callable` lambda submitted to an executor; no `Command` class needed. - **Template Method's hooks** → instead of subclassing to override a hook, pass the hook as a lambda parameter (favouring composition over inheritance). - **Observer** → a listener registered as a lambda (`button.addActionListener(e -> ...)`). - **Factory** → a `Supplier<T>` is a zero-arg factory; `Map.computeIfAbsent(key, k -> new Value())` passes a factory lambda. ## Composition: a new capability Functional interfaces ship **default methods** that compose behaviour, which the class-based form never offered ergonomically: `Comparator.comparing(User::lastName).thenComparing(User::firstName).reversed()`, or `predicate1.and(predicate2)`, or `function1.andThen(function2)`. You build complex strategies by combining small ones — a functional-composition style. ## Trade-offs (when a class still wins) Lambdas aren't always the answer: - **No good name in stack traces** — a lambda shows as `lambda$method$0`; a named class is self-documenting in exceptions and profilers. - **State and reuse** — if the strategy carries configuration or is reused across the codebase, a named class (or a `static final` named lambda) is clearer than an inline literal. - **Multiple methods** — if the abstraction genuinely needs more than one method, it isn't a functional interface and a lambda can't express it. - **Readability for non-trivial bodies** — a 15-line lambda is worse than a named method reference. ## The bigger picture Lambdas make **behaviour a first-class value** in Java — you pass, store, and compose functions like data. This shifts idiomatic Java toward composition and away from the inheritance-heavy, class-per-strategy style of pre-8 code. It dovetails with composition-over-inheritance and KISS: the lightest expression of pluggable behaviour is usually a lambda, reserving named classes for the cases that genuinely need identity, state, or multiple methods.

  • Does using a lambda mean you've stopped using the Strategy pattern?
    No — the pattern is about *intent* (pluggable, runtime-chosen behaviour behind an abstraction), not about whether a named class exists. A `Comparator` lambda is still a Strategy; the abstraction (the `Comparator` interface) is still there. The lambda is just a lighter way to supply the concrete strategy.
  • When would you still prefer a named class over a lambda for a strategy?
    When the strategy carries configuration/state, is reused in many places (a single named definition is clearer than copies), needs a meaningful name in stack traces/profilers, has a non-trivial body that hurts inline readability, or the abstraction genuinely needs more than one method (so it isn't a functional interface at all).

saying these in an interview costs you the question

  • Claiming lambdas replaced the Strategy pattern — they replaced the *boilerplate*, not the pattern.
  • Saying any interface can be a lambda — only single-abstract-method (functional) interfaces can.
  • Thinking `@FunctionalInterface` is required for a lambda — it's optional; it only enforces the SAM rule.
  • Believing lambdas can capture and mutate local variables — captured locals must be effectively final.
  • Using a huge multi-line lambda where a named method reference would be far clearer.

context