Behavioural and structural patterns in modern Java — most of them are a lambda or a small interface away.
Strategy
import java.util.Map;
import java.util.function.DoubleUnaryOperator;
public class Main {
static final Map<String, DoubleUnaryOperator> DISCOUNTS = Map.of(
"none", price -> price,
"festive", price -> price * 0.8,
"flat50", price -> Math.max(0, price - 50));
static double checkout(double price, String code) {
return DISCOUNTS.getOrDefault(code, DISCOUNTS.get("none")).applyAsDouble(price);
}
public static void main(String[] args) {
System.out.println(checkout(200, "festive"));
System.out.println(checkout(200, "flat50"));
System.out.println(checkout(200, "bogus"));
}
}160.0
150.0
200.0Comparator is the JDK's best-known strategy: list.sort(Comparator.comparing(Person::age)) plugs a comparison algorithm into a sort.
Observer
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class Main {
static final class StockTicker {
private final List<Consumer<Double>> listeners = new ArrayList<>();
Runnable subscribe(Consumer<Double> l) {
listeners.add(l);
return () -> listeners.remove(l); // handle to unsubscribe
}
void publish(double price) {
listeners.forEach(l -> l.accept(price));
}
}
public static void main(String[] args) {
StockTicker ticker = new StockTicker();
ticker.subscribe(p -> System.out.println("display: " + p));
Runnable cancel = ticker.subscribe(p -> {
if (p > 100) System.out.println("alert: above 100");
});
ticker.publish(99.5);
ticker.publish(101.0);
cancel.run(); // stop the alerts
ticker.publish(120.0);
}
}display: 99.5
display: 101.0
alert: above 100
display: 120.0Decorator
Wrap an object in another object with the same interface to add behaviour. java.io is built this way: each stream adds one feature (buffering, decoding, line reading).
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
public class Main {
interface Coffee { double cost(); String describe(); }
record Espresso() implements Coffee {
public double cost() { return 2.0; }
public String describe() { return "espresso"; }
}
record WithMilk(Coffee inner) implements Coffee {
public double cost() { return inner.cost() + 0.5; }
public String describe() { return inner.describe() + " + milk"; }
}
record WithSyrup(Coffee inner) implements Coffee {
public double cost() { return inner.cost() + 0.75; }
public String describe() { return inner.describe() + " + syrup"; }
}
public static void main(String[] args) throws IOException {
Coffee order = new WithSyrup(new WithMilk(new Espresso()));
System.out.println(order.describe() + " = " + order.cost());
// the JDK version: a Reader decorated with buffering and readLine()
try (BufferedReader r = new BufferedReader(new StringReader("line 1\nline 2"))) {
System.out.println(r.readLine());
}
}
}espresso + milk + syrup = 3.25
line 1Template method
An abstract class fixes the skeleton of an algorithm in a final method and lets subclasses fill in the steps.
public class Main {
static abstract class Report {
final String render() { // the template — not overridable
return header() + "\n" + body() + "\n-- end --";
}
String header() { return "REPORT"; } // hook with a default
abstract String body(); // step subclasses must supply
}
static final class SalesReport extends Report {
@Override String body() { return "sales: 42 units"; }
}
public static void main(String[] args) {
System.out.println(new SalesReport().render());
}
}REPORT
sales: 42 units
-- end --Adapter
Convert one interface into another that a client expects. Arrays.asList adapts an array to List; InputStreamReader adapts a byte stream to a character Reader.
import java.util.function.Supplier;
public class Main {
static final class LegacyThermometer { // can't change this class
double readFahrenheit() { return 98.6; }
}
interface CelsiusSensor { double celsius(); }
static CelsiusSensor adapt(LegacyThermometer t) { // the adapter
return () -> (t.readFahrenheit() - 32) * 5 / 9;
}
public static void main(String[] args) {
CelsiusSensor s = adapt(new LegacyThermometer());
Supplier<String> fmt = () -> String.format("%.1f C", s.celsius());
System.out.println(fmt.get());
}
}37.0 CWhere the JDK uses them
| Pattern | Kind | In the JDK |
|---|---|---|
| Strategy | Behavioural | Comparator, ThreadPoolExecutor's RejectedExecutionHandler |
| Observer | Behavioural | PropertyChangeListener, Swing listeners, Flow.Subscriber |
| Iterator | Behavioural | Iterator / Iterable behind every for-each loop |
| Template method | Behavioural | AbstractList (get + size → everything else), InputStream.read(byte[]) |
| Command | Behavioural | Runnable, Callable submitted to executors |
| Decorator | Structural | BufferedInputStream, Collections.unmodifiableList, synchronizedMap |
| Adapter | Structural | Arrays.asList, InputStreamReader, OutputStreamWriter |
| Proxy | Structural | java.lang.reflect.Proxy (Spring AOP, mocks) |
| Factory / static factory | Creational | List.of, Calendar.getInstance, NumberFormat.getInstance |
| Builder | Creational | StringBuilder, HttpClient.newBuilder() |
| Singleton | Creational | Runtime.getRuntime() |
Strategy vs template method?
Decorator vs inheritance for adding behaviour?