Creational patterns focus on object creation mechanisms, aiming to create objects in a manner suitable to the situation. They help make a system independent of how its objects are created, composed, and represented.
The five creational patterns covered are:
- Singleton Pattern
- Factory Method Pattern
- Abstract Factory Pattern
- Prototype Pattern
- Builder Pattern
Singleton Pattern
The Singleton pattern ensures a class has only one instance, while providing global access to that instance. It's commonly used for logging, driver objects, caching, thread pools, and database connections.
Implementation Approaches
Eager Initialization
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
This approach creates the instance at class loading time, ensuring thread safety but potentially wasting memory if the instance is never used.
Lazy Initialization with Double-Checked Locking
public class LazySingleton {
private static volatile LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
The volatile keyword ensures visibility across threads and prevents instruction reordering issues.
Static Inner Class Approach
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {}
private static class SingletonHolder {
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
This leverages JVM's class loading mechanism to ensure thread safety without synchronization overhead.
Enum Singleton
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
// implementation
}
}
Enum-based singletons are inherently thread-safe and immune to serialization attacks.
Handling Serialization and Reflection Issues
To prevent breaking singleton through deserialization:
public class SerializableSingleton implements Serializable {
private static final long serialVersionUID = 1L;
private SerializableSingleton() {}
private static class SingletonHelper {
private static final SerializableSingleton INSTANCE = new SerializableSingleton();
}
public static SerializableSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
private Object readResolve() {
return SingletonHelper.INSTANCE;
}
}
To prevent reflection-based instantiation:
public class SecureSingleton {
private static volatile SecureSingleton instance;
private SecureSingleton() {
if (instance != null) {
throw new RuntimeException("Use getInstance() method to create instance.");
}
}
public static SecureSingleton getInstance() {
if (instance == null) {
synchronized (SecureSingleton.class) {
if (instance == null) {
instance = new SecureSingleton();
}
}
}
return instance;
}
}
Factory Method Pattern
Factory Method defines an interface for creating an object, but lets subclasses decide wich class to instantiate.
Structure
// Product interface
interface Coffee {
String getName();
void addIngredients();
}
// Concrete products
class Americano implements Coffee {
public String getName() { return "Americano"; }
public void addIngredients() { /* implementation */ }
}
class Latte implements Coffee {
public String getName() { return "Latte"; }
public void addIngredients() { /* implementation */ }
}
// Creator interface
interface CoffeeFactory {
Coffee createCoffee();
}
// Concrete creators
class AmericanoFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new Americano();
}
}
class LatteFactory implements CoffeeFactory {
public Coffee createCoffee() {
return new Latte();
}
}
// Usage
public class Cafe {
private CoffeeFactory factory;
public Cafe(CoffeeFactory factory) {
this.factory = factory;
}
public Coffee orderCoffee() {
Coffee coffee = factory.createCoffee();
coffee.addIngredients();
return coffee;
}
}
Abstract Factory Pattern
Abstract Factory provides an interface for creating families of related or dependent objects without specifying their concrete classes.
Implementation Example
// Abstract products
interface Beverage {
String getDescription();
}
interface Pastry {
String getType();
}
// Concrete products
class ItalianCoffee implements Beverage {
public String getDescription() { return "Italian Style Coffee"; }
}
class Tiramisu implements Pastry {
public String getType() { return "Tiramisu"; }
}
class AmericanCoffee implements Beverage {
public String getDescription() { return "American Style Coffee"; }
}
class Muffin implements Pastry {
public String getType() { return "Blueberry Muffin"; }
}
// Abstract factory
interface AfternoonTeaFactory {
Beverage createBeverage();
Pastry createPastry();
}
// Concrete factories
class ItalianAfternoonTeaFactory implements AfternoonTeaFactory {
public Beverage createBeverage() {
return new ItalianCoffee();
}
public Pastry createPastry() {
return new Tiramisu();
}
}
class AmericanAfternoonTeaFactory implements AfternoonTeaFactory {
public Beverage createBeverage() {
return new AmericanCoffee();
}
public Pastry createPastry() {
return new Muffin();
}
}
Prototype Pattern
Prototype creates new objects by copying an existing instance, known as the prototype.
Shallow vs Deep Cloning
// Shallow cloning example
public class Certificate implements Cloneable {
private String recipientName;
private Course courseDetails;
public Certificate(String name) {
this.recipientName = name;
}
@Override
protected Certificate clone() throws CloneNotSupportedException {
return (Certificate) super.clone();
}
public void display() {
System.out.println(recipientName + " has completed " + courseDetails.getName());
}
}
// Deep cloning using serialization
public class DeepCopyCertificate implements Serializable {
private String recipientName;
private Course courseDetails;
public DeepCopyCertificate deepCopy() throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (DeepCopyCertificate) ois.readObject();
}
}
Builder Pattern
Builder separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
Traditional Builder Implementation
// Product
public class Bicycle {
private String frame;
private String seat;
// Getters and setters
public String getFrame() { return frame; }
public void setFrame(String frame) { this.frame = frame; }
public String getSeat() { return seat; }
public void setSeat(String seat) { this.seat = seat; }
}
// Abstract Builder
abstract class BicycleBuilder {
protected Bicycle bicycle = new Bicycle();
public abstract void buildFrame();
public abstract void buildSeat();
public abstract Bicycle getBicycle();
}
// Concrete Builders
class MountainBikeBuilder extends BicycleBuilder {
public void buildFrame() {
bicycle.setFrame("Aluminum Alloy Frame");
}
public void buildSeat() {
bicycle.setSeat("Gel Seat");
}
public Bicycle getBicycle() {
return bicycle;
}
}
class RoadBikeBuilder extends BicycleBuilder {
public void buildFrame() {
bicycle.setFrame("Carbon Fiber Frame");
}
public void buildSeat() {
bicycle.setSeat("Lightweight Seat");
}
public Bicycle getBicycle() {
return bicycle;
}
}
// Director
public class BicycleAssembler {
private BicycleBuilder builder;
public BicycleAssembler(BicycleBuilder builder) {
this.builder = builder;
}
public Bicycle construct() {
builder.buildFrame();
builder.buildSeat();
return builder.getBicycle();
}
}
// Usage
public class Client {
public static void main(String[] args) {
BicycleBuilder mountainBuilder = new MountainBikeBuilder();
BicycleAssembler assembler = new BicycleAssembler(mountainBuilder);
Bicycle bike = assembler.construct();
System.out.println(bike.getFrame());
}
}
Fluent Builder Pattern
public class Smartphone {
private String processor;
private String screen;
private String memory;
private String battery;
private Smartphone(Builder builder) {
this.processor = builder.processor;
this.screen = builder.screen;
this.memory = builder.memory;
this.battery = builder.battery;
}
public static class Builder {
private String processor;
private String screen;
private String memory;
private String battery;
public Builder processor(String processor) {
this.processor = processor;
return this;
}
public Builder screen(String screen) {
this.screen = screen;
return this;
}
public Builder memory(String memory) {
this.memory = memory;
return this;
}
public Builder battery(String battery) {
this.battery = battery;
return this;
}
public Smartphone build() {
return new Smartphone(this);
}
}
}
// Usage
Smartphone phone = new Smartphone.Builder()
.processor("Snapdragon 888")
.screen("6.7 inch OLED")
.memory("12GB RAM")
.battery("5000mAh")
.build();
Pattern Comparison
Factory Method vs Builder
Factory Method focuses on creating complete objects at once, while Builder emphasizes step-by-step construction of complex objects.
Abstract Factory vs Builder
Abstract Factory produces families of related objects without concern for assembly sequence, whereas Builder constructs objects following specific blueprints through sequential steps.