The Strategy Pattern is a classic behavioral design pattern that enables selecting an algorithm’s behavior at runtime. Traditionally, this involves defining a family of algorithms, encapsulating each one in its own class, and making them interchangeable via a common interface. This allows the client to choose or switch the algorithm without changing the context code.
Typically, you create an interface representing the strategy, e.g.,
interface SortingStrategy {
void sort(int[] array);
}
Then provide multiple implementations:
class BubbleSort implements SortingStrategy {
public void sort(int[] array) {
// Bubble sort implementation
}
}
class QuickSort implements SortingStrategy {
public void sort(int[] array) {
// Quick sort implementation
}
}
The client code uses the strategy via the interface:
class Sorter {
private SortingStrategy strategy;
public Sorter(SortingStrategy strategy) {
this.strategy = strategy;
}
public void sortArray(int[] array) {
strategy.sort(array);
}
}
While effective, this pattern involves boilerplate classes and verbosity.
Java 8’s introduction of functional interfaces and lambdas greatly simplifies the Strategy pattern. Instead of creating multiple concrete classes, you can define a single functional interface and pass behavior as a lambda expression directly.
For example, the SortingStrategy
interface can be replaced by a functional interface like:
@FunctionalInterface
interface SortingStrategy {
void sort(int[] array);
}
Using lambdas, you can now easily provide different strategies inline without separate classes:
SortingStrategy bubbleSort = array -> {
// Bubble sort logic
for (int i = 0; i < array.length - 1; i++) {
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
};
SortingStrategy quickSort = array -> Arrays.sort(array); // Using built-in sort
The client can switch strategies effortlessly:
class Sorter {
private SortingStrategy strategy;
public Sorter(SortingStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(SortingStrategy strategy) {
this.strategy = strategy;
}
public void sort(int[] array) {
strategy.sort(array);
}
}
Usage:
public static void main(String[] args) {
int[] data = {5, 2, 8, 3, 1};
Sorter sorter = new Sorter(bubbleSort);
sorter.sort(data);
System.out.println("Bubble sorted: " + Arrays.toString(data));
sorter.setStrategy(quickSort);
int[] data2 = {9, 7, 4, 6};
sorter.sort(data2);
System.out.println("Quick sorted: " + Arrays.toString(data2));
}
import java.util.Arrays;
// Define functional interface for sorting strategies
@FunctionalInterface
interface SortingStrategy {
void sort(int[] array);
}
// Sorter class using a strategy
class Sorter {
private SortingStrategy strategy;
public Sorter(SortingStrategy strategy) {
this.strategy = strategy;
}
public void setStrategy(SortingStrategy strategy) {
this.strategy = strategy;
}
public void sort(int[] array) {
strategy.sort(array);
}
}
public class StrategyPatternWithLambda {
public static void main(String[] args) {
// Lambda-based bubble sort strategy
SortingStrategy bubbleSort = array -> {
for (int i = 0; i < array.length - 1; i++) {
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
};
// Lambda-based quick sort strategy using built-in method
SortingStrategy quickSort = Arrays::sort;
// Using bubble sort
int[] data = {5, 2, 8, 3, 1};
Sorter sorter = new Sorter(bubbleSort);
sorter.sort(data);
System.out.println("Bubble sorted: " + Arrays.toString(data));
// Using quick sort
int[] data2 = {9, 7, 4, 6};
sorter.setStrategy(quickSort);
sorter.sort(data2);
System.out.println("Quick sorted: " + Arrays.toString(data2));
}
}
Discount Strategy
@FunctionalInterface
interface DiscountStrategy {
double applyDiscount(double price);
}
DiscountStrategy noDiscount = price -> price;
DiscountStrategy seasonalDiscount = price -> price * 0.9;
DiscountStrategy clearanceDiscount = price -> price * 0.5;
double originalPrice = 100.0;
System.out.println("Seasonal price: " + seasonalDiscount.applyDiscount(originalPrice));
Logging Behavior
@FunctionalInterface
interface Logger {
void log(String message);
}
Logger consoleLogger = msg -> System.out.println("[Console] " + msg);
Logger fileLogger = msg -> System.out.println("[File] " + msg); // Simplified
consoleLogger.log("App started");
fileLogger.log("File saved");
By using Java lambdas and functional interfaces, the Strategy pattern becomes lightweight and flexible. Instead of multiple boilerplate classes, behaviors are expressed concisely as lambdas, making your code more readable and maintainable while retaining full expressiveness and runtime flexibility. This functional approach suits many use cases like sorting, discounts, logging, and more.
The Command Pattern is a behavioral design pattern that encapsulates a request or an operation as an object. This allows you to parameterize clients with different requests, queue or log operations, and support undoable actions. The core idea is to separate the invoker (who triggers the command) from the executor (the code that performs the action), promoting loose coupling and greater flexibility.
Traditionally, you define a Command
interface:
public interface Command {
void execute();
}
Each operation implements this interface as a concrete class:
class TurnOnLightCommand implements Command {
private Light light;
public TurnOnLightCommand(Light light) {
this.light = light;
}
public void execute() {
light.turnOn();
}
}
The invoker holds a reference to the Command
and calls execute()
without knowing the details.
Java’s functional interfaces like Runnable
, Consumer<T>
, or custom ones perfectly fit the Command pattern’s single-method interface. Lambdas enable writing commands concisely without boilerplate classes.
For example, a simple command can be represented as a Runnable
lambda:
Runnable turnOnLight = () -> System.out.println("Light turned ON");
Runnable turnOffLight = () -> System.out.println("Light turned OFF");
An invoker class might simply store and execute these commands:
class RemoteControl {
private Runnable command;
public void setCommand(Runnable command) {
this.command = command;
}
public void pressButton() {
if (command != null) {
command.run();
}
}
}
Usage:
public static void main(String[] args) {
RemoteControl remote = new RemoteControl();
remote.setCommand(turnOnLight);
remote.pressButton(); // Output: Light turned ON
remote.setCommand(turnOffLight);
remote.pressButton(); // Output: Light turned OFF
}
public class CommandPatternWithLambdas {
public static void main(String[] args) {
// Define commands as lambdas
Runnable turnOnLight = () -> System.out.println("Light turned ON");
Runnable turnOffLight = () -> System.out.println("Light turned OFF");
// Create the invoker
RemoteControl remote = new RemoteControl();
// Use the ON command
remote.setCommand(turnOnLight);
remote.pressButton(); // Output: Light turned ON
// Use the OFF command
remote.setCommand(turnOffLight);
remote.pressButton(); // Output: Light turned OFF
}
// Invoker class
static class RemoteControl {
private Runnable command;
public void setCommand(Runnable command) {
this.command = command;
}
public void pressButton() {
if (command != null) {
command.run();
}
}
}
}
Using Runnable
's default method andThen
, you can compose commands:
Runnable startEngine = () -> System.out.println("Engine started");
Runnable playMusic = () -> System.out.println("Music playing");
Runnable morningRoutine = startEngine.andThen(playMusic);
morningRoutine.run();
// Output:
// Engine started
// Music playing
Sometimes commands require parameters or return results. Define a custom interface:
@FunctionalInterface
interface CommandWithArg<T> {
void execute(T arg);
}
CommandWithArg<String> printMessage = msg -> System.out.println("Message: " + msg);
printMessage.execute("Hello, Command Pattern!");
Java functional interfaces simplify the Command pattern by eliminating boilerplate command classes, making commands easy to create, pass, and compose as lambdas. This approach supports deferred execution, flexible workflows, and features like undo or macros, all with concise and readable code. It’s a natural fit for event handling, task scheduling, and any context where actions need to be encapsulated and executed flexibly.
The Observer Pattern is a behavioral design pattern that establishes a one-to-many relationship between objects: when the subject changes its state, all its observers are notified and updated automatically. This pattern is fundamental in event-driven programming, GUIs, and reactive systems where components react asynchronously to events or data changes.
In classic Java, the pattern often involves interfaces like Observer
and Observable
or listener interfaces such as PropertyChangeListener
. Observers implement these interfaces and register themselves with the subject to receive updates.
For example, using PropertyChangeListener
:
PropertyChangeSupport support = new PropertyChangeSupport(this);
support.addPropertyChangeListener(evt -> {
System.out.println("Property " + evt.getPropertyName() + " changed to " + evt.getNewValue());
});
Java 8 introduced lambdas and method references, which simplify observers dramatically by letting you register behavior inline without implementing entire classes. Observers become functional callbacks—just functions invoked on event occurrence.
Instead of writing verbose listener classes, you register a lambda that reacts to events:
subject.addListener(event -> System.out.println("Event received: " + event));
This promotes more readable, concise, and maintainable code.
Let's implement a simple observable subject that accepts functional observers.
import java.util.*;
import java.util.function.Consumer;
class EventSource<T> {
private final List<Consumer<T>> observers = new ArrayList<>();
public void subscribe(Consumer<T> observer) {
observers.add(observer);
}
public void unsubscribe(Consumer<T> observer) {
observers.remove(observer);
}
public void notifyObservers(T event) {
for (Consumer<T> observer : observers) {
observer.accept(event);
}
}
}
Usage:
public class ObserverExample {
public static void main(String[] args) {
EventSource<String> eventSource = new EventSource<>();
// Register observers with lambdas
eventSource.subscribe(event -> System.out.println("Observer 1 received: " + event));
eventSource.subscribe(event -> System.out.println("Observer 2 received: " + event.toUpperCase()));
eventSource.notifyObservers("Hello Observers!");
// Output:
// Observer 1 received: Hello Observers!
// Observer 2 received: HELLO OBSERVERS!
}
}
import java.util.*;
import java.util.function.Consumer;
// Observable subject
class EventSource<T> {
private final List<Consumer<T>> observers = new ArrayList<>();
public void subscribe(Consumer<T> observer) {
observers.add(observer);
}
public void unsubscribe(Consumer<T> observer) {
observers.remove(observer);
}
public void notifyObservers(T event) {
for (Consumer<T> observer : observers) {
observer.accept(event);
}
}
}
// Demo with functional observers
public class ObserverExample {
public static void main(String[] args) {
EventSource<String> eventSource = new EventSource<>();
// Register observers using lambdas
eventSource.subscribe(event -> System.out.println("Observer 1 received: " + event));
eventSource.subscribe(event -> System.out.println("Observer 2 received: " + event.toUpperCase()));
eventSource.notifyObservers("Hello Observers!");
}
}
Because observers are functions, you can store and remove them effortlessly. This is especially useful in long-lived systems where listeners must be unsubscribed to prevent memory leaks:
Consumer<String> observer = event -> System.out.println("Received event: " + event);
eventSource.subscribe(observer);
// Later, unsubscribe if needed
eventSource.unsubscribe(observer);
Functional callbacks transform the Observer pattern into a lightweight, flexible mechanism for event handling. By leveraging lambdas and method references, Java developers can implement event subscription and notification without boilerplate listener classes, making event-driven code more concise, readable, and maintainable. This functional approach aligns perfectly with GUI frameworks, reactive streams, and modern event-driven architectures.
Java’s GUI frameworks, such as Swing, rely heavily on event-driven programming where components notify listeners of user actions like button clicks or text input changes. With Java 8 and later, functional interfaces and lambdas simplify event handling by replacing verbose anonymous classes with concise, readable functional callbacks.
Below is a runnable example demonstrating a simple Swing GUI that uses lambdas to handle button clicks and window closing events.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class FunctionalSwingExample {
public static void main(String[] args) {
// Create main frame (window)
JFrame frame = new JFrame("Functional Event Handling Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(400, 200);
// Create a button
JButton clickButton = new JButton("Click Me");
// Create a label to show click count
JLabel label = new JLabel("Button not clicked yet");
// Use lambda for button click event handler (ActionListener)
clickButton.addActionListener(e -> {
String currentText = label.getText();
int count = 0;
if (currentText.contains("clicked")) {
count = Integer.parseInt(currentText.replaceAll("\\D+", "")) + 1;
}
label.setText("Button clicked " + count + " times");
});
// Use lambda for window close event (WindowListener)
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
System.out.println("Window is closing. Bye!");
}
});
// Layout components vertically
frame.setLayout(new FlowLayout());
frame.add(clickButton);
frame.add(label);
// Show the window
frame.setVisible(true);
}
}
addActionListener
method accepts a functional interface ActionListener
(single method actionPerformed
). Using a lambda expression, the code becomes concise without anonymous inner classes.WindowListener
interface has multiple methods, so we use the adapter class WindowAdapter
and override only the relevant method (windowClosing
) with a lambda-friendly style (anonymous subclass).This example illustrates how functional design patterns mesh naturally with GUI event handling, making your Java desktop apps more elegant and easier to maintain. Lambdas help keep event-driven code clear and focused, boosting developer productivity in GUI development.