Java: strategy and observer with interfaces and lambdas
Swap behavior behind an interface and notify listeners without coupling the sender to them.
On this page
Java interfaces and lambda expressions.
- Swap behavior behind an interface and notify listeners without coupling the sender to them.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
The strategy pattern puts interchangeable behaviors behind one interface, so the caller picks which behavior to use and the code that uses it does not change. A functional interface with one method can be implemented with a lambda, which keeps each strategy short.
The observer pattern lets one object publish events to listeners it knows only as an interface. The publisher does not need to know what the listeners do, which makes it easy to add logging or notifications later. Both patterns depend on abstractions instead of concrete classes.
Code example
import java.util.*;
import java.util.function.*;
class Main {
interface Discount {
double apply(double price);
}
static class EventBus {
private final List<Consumer<String>> listeners = new ArrayList<>();
void subscribe(Consumer<String> listener) {
listeners.add(listener);
}
void publish(String event) {
for (Consumer<String> listener : listeners) {
listener.accept(event);
}
}
}
public static void main(String[] args) {
Discount none = price -> price;
Discount tenPercent = price -> price * 0.9;
Discount[] plans = {none, tenPercent};
for (int i = 0; i < plans.length; i++) {
System.out.println("plan " + i + ": " + plans[i].apply(100.0));
}
EventBus bus = new EventBus();
List<String> log = new ArrayList<>();
bus.subscribe(log::add);
bus.subscribe(event -> System.out.println("got " + event));
bus.publish("order_placed");
System.out.println(log);
}
}plan 0: 100.0 plan 1: 90.0 got order_placed [order_placed]
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
import java.util.*;import java.util.function.*; class Main { interface Discount { double apply(double price); } static class EventBus { private final List<Consumer<String>> listeners = new ArrayList<>(); void subscribe(Consumer<String> listener) { listeners.add(listener); } void publish(String event) { for (Consumer<String> listener : listeners) { listener.accept(event); } } } public static void main(String[] args) { Discount none = price -> price; Discount tenPercent = price -> price * 0.9; Discount[] plans = {none, tenPercent}; for (int i = 0; i < plans.length; i++) { System.out.println("plan " + i + ": " + plans[i].apply(100.0)); } EventBus bus = new EventBus(); List<String> log = new ArrayList<>(); bus.subscribe(log::add); bus.subscribe(event -> System.out.println("got " + event)); bus.publish("order_placed"); System.out.println(log); }}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| A lambda is a second implementation of the same interface. | tenPercent: price * 0.9 | — | |
| The loop works for any Discount without knowing which one it has. | tenPercent: price * 0.9 | plan 0: 100.0 plan 1: 90.0 | |
| The first listener stores events in a list; the second prints them. | listeners = 2 | plan 0: 100.0 plan 1: 90.0 | |
| Both listeners receive the event in the order they subscribed. | listeners = 2 | plan 0: 100.0 plan 1: 90.0 got order_placed | |
| The log listener recorded the event. | listeners = 2 | plan 0: 100.0 plan 1: 90.0 got order_placed [order_placed] |
Behind the scenes
How Java processes this code
The Java compiler produces bytecode. The JVM loads the class and invokes its main method.
Cost and common mistakes
publish is O(L) for L listeners; apply is O(1).
- A listener that throws an exception stops the loop, so later listeners never run. Decide how errors should be handled.
- Listeners hold references, so forgetting to unsubscribe can keep objects alive and leak memory.
- Do not use double for money in real code; BigDecimal or integer cents avoid rounding errors.
Try it yourself
Add a Discount that takes a fixed 15 off.
Reveal answer and reasoning
Discount fifteenOff = price -> price - 15; with 100.0 it returns 85.0, and no existing code changes.