Java Design Patterns offer time-tested solutions to common software design problems — and Java Collections like List
, Map
, and Set
often play an integral role in implementing them effectively.
Whether you're managing a pool of strategies, mapping commands to handlers, or observing event listeners, understanding how collections enhance these patterns makes your code cleaner, more scalable, and maintainable.
This guide walks you through how to use collections in popular design patterns like Factory, Strategy, Observer, Decorator, and Composite, with real-world Java examples.
Why Collections Are Central to Design Patterns
Collections provide:
- Flexible structure for registering and storing components
- Efficient lookup and dispatching with
Map
- Order-preserving or deduplicating behavior via
List
orSet
- Dynamic composition of multiple implementations
Factory Pattern + Map
Problem
You need to instantiate different objects based on input type.
Collection-Based Solution
Use a Map<String, Supplier<Product>>
for lookup-based creation.
Example
interface Product {
void deliver();
}
class Book implements Product {
public void deliver() { System.out.println("Delivering book..."); }
}
class Laptop implements Product {
public void deliver() { System.out.println("Delivering laptop..."); }
}
class ProductFactory {
private static final Map<String, Supplier<Product>> registry = new HashMap<>();
static {
registry.put("book", Book::new);
registry.put("laptop", Laptop::new);
}
public static Product create(String type) {
return registry.getOrDefault(type, () -> () -> System.out.println("Unknown")).get();
}
}
Strategy Pattern + Map or List
Problem
You need to select an algorithm at runtime.
Collection-Based Solution
- Use a
Map<String, Strategy>
for named strategies - Use
List<Strategy>
for composite or ordered strategies
Example
interface PaymentStrategy {
void pay(double amount);
}
class PayPal implements PaymentStrategy {
public void pay(double amount) { System.out.println("Paid via PayPal: " + amount); }
}
class CreditCard implements PaymentStrategy {
public void pay(double amount) { System.out.println("Paid via Credit Card: " + amount); }
}
class PaymentProcessor {
private final Map<String, PaymentStrategy> strategies = Map.of(
"paypal", new PayPal(),
"card", new CreditCard()
);
public void process(String method, double amount) {
strategies.getOrDefault(method, a -> System.out.println("Invalid")).pay(amount);
}
}
Observer Pattern + List
Problem
Notify multiple listeners when an event occurs.
Collection-Based Solution
Use List<Observer>
to track all subscribers.
interface Observer {
void update(String event);
}
class Logger implements Observer {
public void update(String event) { System.out.println("Logged: " + event); }
}
class Notifier implements Observer {
public void update(String event) { System.out.println("Notified user: " + event); }
}
class EventPublisher {
private final List<Observer> observers = new ArrayList<>();
public void subscribe(Observer observer) {
observers.add(observer);
}
public void publish(String event) {
observers.forEach(o -> o.update(event));
}
}
Decorator Pattern + List
Track and apply decorators dynamically using a List
.
interface Coffee {
String serve();
}
class SimpleCoffee implements Coffee {
public String serve() { return "Coffee"; }
}
class MilkDecorator implements Coffee {
private final Coffee coffee;
public MilkDecorator(Coffee coffee) { this.coffee = coffee; }
public String serve() { return coffee.serve() + " + Milk"; }
}
class SugarDecorator implements Coffee {
private final Coffee coffee;
public SugarDecorator(Coffee coffee) { this.coffee = coffee; }
public String serve() { return coffee.serve() + " + Sugar"; }
}
// Usage
Coffee coffee = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
System.out.println(coffee.serve()); // Coffee + Milk + Sugar
Composite Pattern + List
Structure hierarchies using List<Component>
in each node.
interface FileSystemComponent {
void display();
}
class FileLeaf implements FileSystemComponent {
private final String name;
public FileLeaf(String name) { this.name = name; }
public void display() { System.out.println(name); }
}
class Folder implements FileSystemComponent {
private final String name;
private final List<FileSystemComponent> children = new ArrayList<>();
public Folder(String name) { this.name = name; }
public void add(FileSystemComponent c) {
children.add(c);
}
public void display() {
System.out.println("Folder: " + name);
children.forEach(FileSystemComponent::display);
}
}
Java Version Tracker
📌 What's New in Java?
- Java 8
Map.computeIfAbsent()
,List.forEach()
, and lambdas made pattern use easier
- Java 9
- Immutable
Map.of()
andList.of()
simplified pattern bootstrapping
- Immutable
- Java 10+
var
for cleaner factory and strategy code
- Java 21
- Virtual threads allow better async observers and event processors
Performance Considerations
Pattern | Common Collection | Access Time | Thread-Safe Alternatives |
---|---|---|---|
Factory | Map<String, Supplier> |
O(1) | ConcurrentHashMap |
Strategy | Map<String, Strategy> |
O(1) | EnumMap , ConcurrentMap |
Observer | List<Observer> |
O(n) | CopyOnWriteArrayList |
Composite | List<Component> |
O(n) | Usually not concurrent |
Best Practices
- Use
Map<String, T>
for fast dispatch (Factory, Strategy) - Favor
List<T>
for ordered composition (Composite, Observer) - Always return unmodifiable collections in public APIs
- Use lazy loading with
computeIfAbsent()
where needed - Avoid modifying collections directly during iteration
Anti-Patterns
- Using raw types (
Map
instead ofMap<String, T>
) - Not validating collection keys in factories
- Forgetting to handle
null
values in strategy maps - Returning internal modifiable collections from services
Refactoring Legacy Code
- Replace
if-else
orswitch
withMap<String, Function>
for factories - Convert listener lists to
CopyOnWriteArrayList
for thread safety - Move from nested inheritance to composite structure with
List
Real-World Use Cases
- Spring ApplicationContext: BeanFactory registry using
Map<String, Object>
- Command Dispatchers:
Map<CommandType, CommandHandler>
for REST controllers - Metrics Collection: Observer pattern with listener list
- Middleware Chains: Strategy + Decorator pattern via ordered
List
Conclusion and Key Takeaways
- Java Collections are the backbone of many design patterns
Map
is powerful for lookup-based dispatchingList
enables composite hierarchies, observers, and decorators- Knowing how to leverage collections inside patterns reduces complexity and increases flexibility
FAQ – Java Collections in Design Patterns
-
Which design patterns use Map most effectively?
Factory and Strategy — for quick object lookup and behavior switching. -
Why use List in Observer or Composite patterns?
It maintains order and supports hierarchical relationships. -
How do I ensure thread safety in these patterns?
Use concurrent collections likeConcurrentHashMap
,CopyOnWriteArrayList
. -
Can I use EnumMap for strategies?
Yes — if your keys are enums, it's faster and more memory-efficient. -
Should collections be modifiable in design pattern objects?
No — protect internal collections with unmodifiable wrappers. -
How do I replace switch-case with a collection?
UseMap<String, Runnable>
orMap<Enum, Command>
. -
Is there a performance hit using Map for factories?
Negligible —HashMap
offers constant-time lookup. -
Are these collections serializable?
Yes — but test custom strategies or lambdas separately. -
Can I inject these maps via Spring or DI?
Absolutely — use@Bean
methods or component scanning. -
What if I need to remove listeners dynamically?
Useremove()
on yourList<Observer>
implementation safely.