Interview

15 Java Design Patterns Interview Questions and Answers

Prepare for your next technical interview with this guide on Java Design Patterns, featuring common questions and expert answers.

Java Design Patterns are essential tools for any developer aiming to write efficient, maintainable, and scalable code. These patterns provide proven solutions to common software design problems, making it easier to develop robust applications. Understanding and implementing design patterns can significantly improve your coding practices and enhance your ability to tackle complex software projects.

This article offers a curated selection of interview questions focused on Java Design Patterns. By familiarizing yourself with these questions and their answers, you’ll be better prepared to demonstrate your expertise and problem-solving abilities in technical interviews.

Java Design Patterns Interview Questions and Answers

1. Explain the Singleton Pattern and its use cases.

The Singleton Pattern is a creational design pattern that ensures a class has only one instance and provides a global point of access to it. This is useful when a single instance is needed to control actions throughout a program. Common use cases include configuration settings, logging, and thread pools.

Here is a simple implementation of the Singleton Pattern in Java:

public class Singleton {
    private static Singleton instance;

    private Singleton() {
        // private constructor to prevent instantiation
    }

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

In this example, the constructor is private, preventing instantiation from outside. The getInstance method checks if the instance is null and creates a new one if it is. Otherwise, it returns the existing instance.

2. What is the difference between Abstract Factory Pattern and Factory Method Pattern?

The Abstract Factory Pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. It is used when multiple related objects need to be created together. This pattern is useful when the system needs to be independent of how its objects are created, composed, and represented.

The Factory Method Pattern defines an interface for creating an object but allows subclasses to alter the type of objects that will be created. It is used when a class cannot anticipate the class of objects it needs to create.

Example of Abstract Factory Pattern:

interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

class WinFactory implements GUIFactory {
    public Button createButton() {
        return new WinButton();
    }
    public Checkbox createCheckbox() {
        return new WinCheckbox();
    }
}

class MacFactory implements GUIFactory {
    public Button createButton() {
        return new MacButton();
    }
    public Checkbox createCheckbox() {
        return new MacCheckbox();
    }
}

Example of Factory Method Pattern:

abstract class Dialog {
    public void render() {
        Button okButton = createButton();
        okButton.render();
    }
    protected abstract Button createButton();
}

class WindowsDialog extends Dialog {
    protected Button createButton() {
        return new WinButton();
    }
}

class WebDialog extends Dialog {
    protected Button createButton() {
        return new HTMLButton();
    }
}

3. How does the Decorator Pattern enhance or modify object behavior?

The Decorator Pattern works by creating a set of decorator classes that wrap concrete components. These decorator classes mirror the type of the components they decorate, allowing them to be used interchangeably. This pattern adheres to the Open/Closed Principle, which states that classes should be open for extension but closed for modification.

Example:

// Component Interface
public interface Coffee {
    String getDescription();
    double getCost();
}

// Concrete Component
public class SimpleCoffee implements Coffee {
    public String getDescription() {
        return "Simple Coffee";
    }

    public double getCost() {
        return 5.0;
    }
}

// Decorator Class
public abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee coffee) {
        this.decoratedCoffee = coffee;
    }

    public String getDescription() {
        return decoratedCoffee.getDescription();
    }

    public double getCost() {
        return decoratedCoffee.getCost();
    }
}

// Concrete Decorators
public class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    public String getDescription() {
        return decoratedCoffee.getDescription() + ", Milk";
    }

    public double getCost() {
        return decoratedCoffee.getCost() + 1.5;
    }
}

public class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    public String getDescription() {
        return decoratedCoffee.getDescription() + ", Sugar";
    }

    public double getCost() {
        return decoratedCoffee.getCost() + 0.5;
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        Coffee coffee = new SimpleCoffee();
        coffee = new MilkDecorator(coffee);
        coffee = new SugarDecorator(coffee);

        System.out.println(coffee.getDescription()); // Simple Coffee, Milk, Sugar
        System.out.println(coffee.getCost()); // 7.0
    }
}

4. Describe the Adapter Pattern and how you would implement it in Java.

The Adapter Pattern allows two incompatible interfaces to work together by creating an adapter class that converts the interface of a class into another interface that a client expects. This pattern is used when integrating new components into an existing system without modifying the existing code.

Here is a concise example of how the Adapter Pattern can be implemented in Java:

// Existing interface
interface MediaPlayer {
    void play(String audioType, String fileName);
}

// New interface
interface AdvancedMediaPlayer {
    void playVlc(String fileName);
    void playMp4(String fileName);
}

// Concrete implementation of the new interface
class VlcPlayer implements AdvancedMediaPlayer {
    public void playVlc(String fileName) {
        System.out.println("Playing vlc file. Name: " + fileName);
    }
    public void playMp4(String fileName) {
        // Do nothing
    }
}

class Mp4Player implements AdvancedMediaPlayer {
    public void playMp4(String fileName) {
        System.out.println("Playing mp4 file. Name: " + fileName);
    }
    public void playVlc(String fileName) {
        // Do nothing
    }
}

// Adapter class implementing the existing interface
class MediaAdapter implements MediaPlayer {
    AdvancedMediaPlayer advancedMusicPlayer;

    public MediaAdapter(String audioType) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer = new VlcPlayer();
        } else if (audioType.equalsIgnoreCase("mp4")) {
            advancedMusicPlayer = new Mp4Player();
        }
    }

    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("vlc")) {
            advancedMusicPlayer.playVlc(fileName);
        } else if (audioType.equalsIgnoreCase("mp4")) {
            advancedMusicPlayer.playMp4(fileName);
        }
    }
}

// Client class using the adapter
class AudioPlayer implements MediaPlayer {
    MediaAdapter mediaAdapter;

    public void play(String audioType, String fileName) {
        if (audioType.equalsIgnoreCase("mp3")) {
            System.out.println("Playing mp3 file. Name: " + fileName);
        } else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
            mediaAdapter = new MediaAdapter(audioType);
            mediaAdapter.play(audioType, fileName);
        } else {
            System.out.println("Invalid media. " + audioType + " format not supported");
        }
    }
}

5. Explain the Prototype Pattern and provide an example scenario where it would be useful.

The Prototype Pattern involves implementing a prototype interface that defines a method for cloning objects. This pattern is useful when creating a new object is resource-intensive, and a copy of an existing object can be made instead.

Example Scenario: Consider a scenario where you have a complex object that is expensive to create, such as a large graphic object with many attributes. Instead of creating a new instance every time, you can clone an existing instance.

import java.util.HashMap;
import java.util.Map;

interface Prototype {
    Prototype clone();
}

class Graphic implements Prototype {
    private String color;
    private String shape;

    public Graphic(String color, String shape) {
        this.color = color;
        this.shape = shape;
    }

    @Override
    public Prototype clone() {
        return new Graphic(this.color, this.shape);
    }

    @Override
    public String toString() {
        return "Graphic [color=" + color + ", shape=" + shape + "]";
    }
}

class GraphicRegistry {
    private Map<String, Graphic> graphics = new HashMap<>();

    public void addGraphic(String key, Graphic graphic) {
        graphics.put(key, graphic);
    }

    public Graphic getGraphic(String key) {
        return (Graphic) graphics.get(key).clone();
    }
}

public class PrototypePatternDemo {
    public static void main(String[] args) {
        GraphicRegistry registry = new GraphicRegistry();
        registry.addGraphic("circle", new Graphic("red", "circle"));

        Graphic clonedGraphic = registry.getGraphic("circle");
        System.out.println(clonedGraphic);
    }
}

6. Describe the Command Pattern and provide a use case.

The Command Pattern encapsulates all the details of an operation, including the method to call, the method’s arguments, and the object to which the method belongs. This pattern decouples the object that invokes the operation from the one that knows how to perform it.

A common use case for the Command Pattern is implementing a text editor with undo and redo functionality. Each command (e.g., typing a character, deleting a character) can be encapsulated as an object, allowing the editor to keep a history of commands and easily undo or redo them.

Example:

// Command Interface
public interface Command {
    void execute();
    void undo();
}

// Concrete Command
public class WriteCommand implements Command {
    private Document document;
    private String text;

    public WriteCommand(Document document, String text) {
        this.document = document;
        this.text = text;
    }

    @Override
    public void execute() {
        document.write(text);
    }

    @Override
    public void undo() {
        document.eraseLast();
    }
}

// Receiver
public class Document {
    private StringBuilder content = new StringBuilder();

    public void write(String text) {
        content.append(text);
    }

    public void eraseLast() {
        content.deleteCharAt(content.length() - 1);
    }

    @Override
    public String toString() {
        return content.toString();
    }
}

// Invoker
public class Editor {
    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void executeCommand() {
        command.execute();
    }

    public void undoCommand() {
        command.undo();
    }
}

// Client
public class Client {
    public static void main(String[] args) {
        Document document = new Document();
        Command writeCommand = new WriteCommand(document, "Hello");
        
        Editor editor = new Editor();
        editor.setCommand(writeCommand);
        editor.executeCommand();
        
        System.out.println(document); // Output: Hello
        
        editor.undoCommand();
        System.out.println(document); // Output: Hell
    }
}

7. How would you implement a Chain of Responsibility Pattern?

The Chain of Responsibility pattern involves creating a chain of handler objects, each of which can either handle the request or pass it to the next handler in the chain. This pattern is useful in scenarios where multiple objects can handle a request, but the specific handler is not known in advance.

Example:

abstract class Handler {
    protected Handler nextHandler;

    public void setNextHandler(Handler nextHandler) {
        this.nextHandler = nextHandler;
    }

    public abstract void handleRequest(String request);
}

class ConcreteHandlerA extends Handler {
    public void handleRequest(String request) {
        if (request.equals("A")) {
            System.out.println("Handler A processed the request.");
        } else if (nextHandler != null) {
            nextHandler.handleRequest(request);
        }
    }
}

class ConcreteHandlerB extends Handler {
    public void handleRequest(String request) {
        if (request.equals("B")) {
            System.out.println("Handler B processed the request.");
        } else if (nextHandler != null) {
            nextHandler.handleRequest(request);
        }
    }
}

public class ChainOfResponsibilityDemo {
    public static void main(String[] args) {
        Handler handlerA = new ConcreteHandlerA();
        Handler handlerB = new ConcreteHandlerB();

        handlerA.setNextHandler(handlerB);

        handlerA.handleRequest("A");
        handlerA.handleRequest("B");
        handlerA.handleRequest("C");
    }
}

In this example, we have an abstract Handler class with a method to set the next handler in the chain and an abstract method handleRequest to process the request. ConcreteHandlerA and ConcreteHandlerB are concrete implementations of the Handler class, each capable of handling specific types of requests. The ChainOfResponsibilityDemo class demonstrates how to set up the chain and process requests.

8. Explain the Flyweight Pattern and its benefits.

The Flyweight Pattern is a structural design pattern that aims to minimize memory usage by sharing as much data as possible with similar objects. This pattern is useful when dealing with a large number of objects that share common properties. The key idea is to store shared data externally and pass it to the flyweight objects when needed.

The Flyweight Pattern consists of two main components:

  • Flyweight: The shared object that contains intrinsic state (common data).
  • Flyweight Factory: Manages and creates flyweight objects, ensuring that they are shared properly.

Example:

import java.util.HashMap;
import java.util.Map;

// Flyweight interface
interface Shape {
    void draw();
}

// Concrete Flyweight class
class Circle implements Shape {
    private String color;
    private int x;
    private int y;
    private int radius;

    public Circle(String color) {
        this.color = color;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public void setRadius(int radius) {
        this.radius = radius;
    }

    @Override
    public void draw() {
        System.out.println("Drawing Circle [Color: " + color + ", x: " + x + ", y: " + y + ", radius: " + radius + "]");
    }
}

// Flyweight Factory class
class ShapeFactory {
    private static final Map<String, Shape> circleMap = new HashMap<>();

    public static Shape getCircle(String color) {
        Circle circle = (Circle) circleMap.get(color);

        if (circle == null) {
            circle = new Circle(color);
            circleMap.put(color, circle);
            System.out.println("Creating circle of color: " + color);
        }
        return circle;
    }
}

// Client code
public class FlyweightPatternDemo {
    private static final String[] colors = {"Red", "Green", "Blue", "White", "Black"};

    public static void main(String[] args) {
        for (int i = 0; i < 20; ++i) {
            Circle circle = (Circle) ShapeFactory.getCircle(getRandomColor());
            circle.setX(getRandomX());
            circle.setY(getRandomY());
            circle.setRadius(100);
            circle.draw();
        }
    }

    private static String getRandomColor() {
        return colors[(int) (Math.random() * colors.length)];
    }

    private static int getRandomX() {
        return (int) (Math.random() * 100);
    }

    private static int getRandomY() {
        return (int) (Math.random() * 100);
    }
}

9. Describe the Template Method Pattern and its advantages.

The Template Method Pattern is used to define the basic steps of an algorithm and allow subclasses to provide the implementation for one or more steps. This pattern is useful when you have multiple classes that share a common structure but differ in specific behaviors. By using the Template Method Pattern, you can avoid code duplication and promote code reuse.

Advantages of the Template Method Pattern include:

  • Code Reusability: Common code is centralized in a single place, reducing duplication.
  • Flexibility: Subclasses can override specific steps of the algorithm without altering its overall structure.
  • Maintainability: Changes to the algorithm’s structure need to be made only in the template method, making the code easier to maintain.

Example:

abstract class DataProcessor {
    // Template method
    public final void process() {
        readData();
        processData();
        writeData();
    }

    abstract void readData();
    abstract void processData();
    abstract void writeData();
}

class CSVDataProcessor extends DataProcessor {
    void readData() {
        System.out.println("Reading data from CSV file");
    }

    void processData() {
        System.out.println("Processing CSV data");
    }

    void writeData() {
        System.out.println("Writing data to CSV file");
    }
}

class XMLDataProcessor extends DataProcessor {
    void readData() {
        System.out.println("Reading data from XML file");
    }

    void processData() {
        System.out.println("Processing XML data");
    }

    void writeData() {
        System.out.println("Writing data to XML file");
    }
}

public class Main {
    public static void main(String[] args) {
        DataProcessor csvProcessor = new CSVDataProcessor();
        csvProcessor.process();

        DataProcessor xmlProcessor = new XMLDataProcessor();
        xmlProcessor.process();
    }
}

10. How would you implement a Mediator Pattern to reduce coupling between classes?

The Mediator Pattern involves creating a mediator interface and concrete mediator class that encapsulates the interactions between different components. Each component communicates with the mediator instead of directly with other components.

Example:

// Mediator Interface
public interface ChatMediator {
    void sendMessage(String msg, User user);
    void addUser(User user);
}

// Concrete Mediator
public class ChatMediatorImpl implements ChatMediator {
    private List<User> users;

    public ChatMediatorImpl() {
        this.users = new ArrayList<>();
    }

    @Override
    public void addUser(User user) {
        this.users.add(user);
    }

    @Override
    public void sendMessage(String msg, User user) {
        for (User u : this.users) {
            // Message should not be received by the user sending it
            if (u != user) {
                u.receive(msg);
            }
        }
    }
}

// User Class
public abstract class User {
    protected ChatMediator mediator;
    protected String name;

    public User(ChatMediator med, String name) {
        this.mediator = med;
        this.name = name;
    }

    public abstract void send(String msg);
    public abstract void receive(String msg);
}

// Concrete User
public class UserImpl extends User {
    public UserImpl(ChatMediator med, String name) {
        super(med, name);
    }

    @Override
    public void send(String msg) {
        System.out.println(this.name + " Sending Message: " + msg);
        mediator.sendMessage(msg, this);
    }

    @Override
    public void receive(String msg) {
        System.out.println(this.name + " Received Message: " + msg);
    }
}

// Usage
public class MediatorPatternDemo {
    public static void main(String[] args) {
        ChatMediator mediator = new ChatMediatorImpl();
        
        User user1 = new UserImpl(mediator, "User1");
        User user2 = new UserImpl(mediator, "User2");
        User user3 = new UserImpl(mediator, "User3");
        User user4 = new UserImpl(mediator, "User4");

        mediator.addUser(user1);
        mediator.addUser(user2);
        mediator.addUser(user3);
        mediator.addUser(user4);

        user1.send("Hello, everyone!");
    }
}

11. Explain the Visitor Pattern and provide an example scenario.

The Visitor Pattern involves three main components: the Visitor interface, ConcreteVisitor classes, and Element classes. The Element classes accept a visitor object and allow the visitor to perform operations on them.

Example scenario: Suppose we have a set of different types of documents (e.g., WordDocument, PdfDocument) and we want to perform various operations like printing and saving on these documents without changing their classes.

// Visitor Interface
interface DocumentVisitor {
    void visit(WordDocument doc);
    void visit(PdfDocument doc);
}

// Concrete Visitor
class PrintVisitor implements DocumentVisitor {
    public void visit(WordDocument doc) {
        System.out.println("Printing Word Document");
    }
    public void visit(PdfDocument doc) {
        System.out.println("Printing PDF Document");
    }
}

// Element Interface
interface Document {
    void accept(DocumentVisitor visitor);
}

// Concrete Elements
class WordDocument implements Document {
    public void accept(DocumentVisitor visitor) {
        visitor.visit(this);
    }
}

class PdfDocument implements Document {
    public void accept(DocumentVisitor visitor) {
        visitor.visit(this);
    }
}

// Client Code
public class VisitorPatternDemo {
    public static void main(String[] args) {
        Document wordDoc = new WordDocument();
        Document pdfDoc = new PdfDocument();
        
        DocumentVisitor printVisitor = new PrintVisitor();
        
        wordDoc.accept(printVisitor);
        pdfDoc.accept(printVisitor);
    }
}

12. Describe the Proxy Pattern and its types.

The Proxy Pattern involves creating a proxy object that has the same interface as the real object. The proxy object controls access to the real object, allowing for additional functionality such as lazy initialization, access control, logging, or caching.

There are several types of Proxy Patterns:

  • Virtual Proxy: Controls access to a resource that is expensive to create. It creates the resource only when it is actually needed.
  • Protection Proxy: Controls access to a resource based on access rights. It ensures that only authorized users can access certain methods of the real object.
  • Remote Proxy: Represents an object that exists in a different address space. It is used in distributed systems to hide the complexity of network communication.
  • Cache Proxy: Provides temporary storage for results of expensive operations to improve performance.
  • Smart Proxy: Adds additional behavior when an object is accessed, such as reference counting or logging.

Example:

interface Image {
    void display();
}

class RealImage implements Image {
    private String filename;

    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk();
    }

    private void loadFromDisk() {
        System.out.println("Loading " + filename);
    }

    public void display() {
        System.out.println("Displaying " + filename);
    }
}

class ProxyImage implements Image {
    private RealImage realImage;
    private String filename;

    public ProxyImage(String filename) {
        this.filename = filename;
    }

    public void display() {
        if (realImage == null) {
            realImage = new RealImage(filename);
        }
        realImage.display();
    }
}

public class ProxyPatternDemo {
    public static void main(String[] args) {
        Image image = new ProxyImage("test_image.jpg");
        image.display(); // Loading and displaying the image
        image.display(); // Only displaying the image
    }
}

13. How would you implement a Strategy Pattern to define a family of algorithms?

The Strategy Pattern involves three main components: the Strategy interface, concrete strategy classes implementing the Strategy interface, and the Context class that uses a Strategy.

Example:

// Strategy interface
public interface Strategy {
    int doOperation(int num1, int num2);
}

// Concrete Strategy classes
public class OperationAdd implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 + num2;
    }
}

public class OperationSubtract implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 - num2;
    }
}

public class OperationMultiply implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 * num2;
    }
}

// Context class
public class Context {
    private Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public int executeStrategy(int num1, int num2) {
        return strategy.doOperation(num1, num2);
    }
}

// Usage
public class StrategyPatternDemo {
    public static void main(String[] args) {
        Context context = new Context(new OperationAdd());
        System.out.println("10 + 5 = " + context.executeStrategy(10, 5));

        context = new Context(new OperationSubtract());
        System.out.println("10 - 5 = " + context.executeStrategy(10, 5));

        context = new Context(new OperationMultiply());
        System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
    }
}

14. Explain the Interpreter Pattern and its typical use cases.

The Interpreter Pattern is a behavioral design pattern that provides a way to evaluate language grammar or expressions. It involves defining a representation for the grammar and an interpreter that uses this representation to interpret sentences in the language.

Typical use cases for the Interpreter Pattern include:

  • Interpreting mathematical expressions
  • Parsing and executing SQL queries
  • Interpreting simple scripting languages
  • Configuration file parsing

Example:

interface Expression {
    int interpret();
}

class Number implements Expression {
    private int number;

    public Number(int number) {
        this.number = number;
    }

    @Override
    public int interpret() {
        return this.number;
    }
}

class Add implements Expression {
    private Expression leftExpression;
    private Expression rightExpression;

    public Add(Expression leftExpression, Expression rightExpression) {
        this.leftExpression = leftExpression;
        this.rightExpression = rightExpression;
    }

    @Override
    public int interpret() {
        return this.leftExpression.interpret() + this.rightExpression.interpret();
    }
}

public class InterpreterPatternDemo {
    public static void main(String[] args) {
        Expression expr = new Add(new Number(5), new Number(10));
        System.out.println("Result: " + expr.interpret()); // Output: Result: 15
    }
}

15. Provide a real-world example where a specific design pattern was applied successfully.

A real-world example of a design pattern applied successfully is the use of the Singleton pattern in a logging framework.

In many applications, logging is a component that needs to be consistent and thread-safe. The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This is useful for logging because it ensures that all parts of the application use the same logging instance, maintaining consistency and avoiding issues related to multiple instances.

In a Java-based application, a logging framework like Log4j can be implemented using the Singleton pattern. The Logger class is designed to be a Singleton, ensuring that all log messages are handled by a single instance of the Logger.

public class Logger {
    private static Logger instance;

    private Logger() {
        // private constructor to prevent instantiation
    }

    public static Logger getInstance() {
        if (instance == null) {
            instance = new Logger();
        }
        return instance;
    }

    public void log(String message) {
        // log message to console or file
        System.out.println(message);
    }
}

// Usage
Logger logger = Logger.getInstance();
logger.log("This is a log message.");
Previous

15 Azure Kubernetes Interview Questions and Answers

Back to Interview
Next

23 Kubernetes Interview Questions and Answers