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.
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.
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.
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(); } }
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 } }
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"); } } }
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); } }
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 } }
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.
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:
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); } }
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:
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(); } }
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!"); } }
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); } }
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:
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 } }
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)); } }
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:
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 } }
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.");