Add interview module: 30 design pattern interview questions with tests (Java 25, Spring 7.0.10)

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ic4b7rnC7jppGGdHGESvy
This commit is contained in:
Claude
2026-10-28 18:10:09 +00:00
parent f5688a6b32
commit c51641b7db
48 changed files with 1617 additions and 1 deletions
+1
View File
@@ -0,0 +1 @@
target/
+64
View File
@@ -0,0 +1,64 @@
# interview: 30 design pattern interview questions, each with a test
Companion module for the article
[Top 30 Design Patterns Interview Questions in Java (with Code)](https://ankurm.com/design-patterns-interview-questions-java/).
Every claim in the article is an assertion in a test here; each test also writes the facts it checked to `output/`.
Unlike the rest of this repository (plain `javac`), this module uses Maven because the tests need JUnit and Spring Framework.
| | |
|---|---|
| JDK | 25 (Temurin 25.0.5) |
| Spring Framework | 7.0.10 (`spring-context`, `spring-tx`) |
| JUnit | 6.1.3 |
| AssertJ | 3.27.7 |
## Run
```bash
cd interview
./scripts/run-all.sh # runs mvn test, regenerates every file in output/
```
## Questions
| Q | Topic | Source | Test | Output |
|---|---|---|---|---|
| 1 | Eager singleton | `singleton/EagerSingleton.java` | `SingletonInterviewTest` | `01-singleton.txt` |
| 2 | Lazy singleton is not thread-safe | `singleton/NaiveLazySingleton.java` | `SingletonInterviewTest` | `01-singleton.txt` |
| 3 | `synchronized` getInstance | `singleton/SynchronizedSingleton.java` | `SingletonInterviewTest` | `01-singleton.txt` |
| 4 | Double-checked locking needs `volatile` | `singleton/DclSingleton.java`, `DclWithoutVolatile.java` | `SingletonInterviewTest` | `01-singleton.txt` |
| 5 | Holder idiom | `singleton/HolderSingleton.java` | `SingletonInterviewTest` | `01-singleton.txt` |
| 6 | Enum singleton vs serialization and reflection | `singleton/EnumSingleton.java`, `ClassicSingleton.java`, `HardenedSingleton.java` | `SingletonInterviewTest` | `01-singleton.txt` |
| 7 | Spring singleton scope vs GoF Singleton | `spring/ScopeDemo.java` | `SingletonInterviewTest` | `01-singleton.txt` |
| 8 | Simple factory vs Factory Method | `creational/SimpleFactory.java`, `Dialog.java` | `CreationalAndBehavioralTest` | `02-creational.txt` |
| 9 | Factory Method vs Abstract Factory | `creational/GuiFactory.java` | `CreationalAndBehavioralTest` | `02-creational.txt` |
| 10 | Builder vs Factory | `creational/HttpCall.java` | `CreationalAndBehavioralTest` | `02-creational.txt` |
| 11 | Prototype: shallow vs deep copy | `creational/Doc.java` | `CreationalAndBehavioralTest` | `02-creational.txt` |
| 12 | Strategy vs State | `behavioral/Order.java`, `Checkout.java` | `CreationalAndBehavioralTest` | `03-behavioral.txt` |
| 13 | Strategy as a lambda | `behavioral/Checkout.java` | `CreationalAndBehavioralTest` | `03-behavioral.txt` |
| 14 | Observer: unsubscribing while notified | `behavioral/EventSource.java` | `CreationalAndBehavioralTest` | `03-behavioral.txt` |
| 15 | Template Method vs Strategy | `behavioral/ReportJob.java` | `CreationalAndBehavioralTest` | `03-behavioral.txt` |
| 16 | Chain of Responsibility | `behavioral/Pipeline.java` | `CreationalAndBehavioralTest` | `03-behavioral.txt` |
| 17 | Command with undo | `behavioral/Editor.java` | `CreationalAndBehavioralTest` | `03-behavioral.txt` |
| 18 | Visitor vs sealed switch | `behavioral/Expr.java`, `src/broken/Q18Broken.java` | `CreationalAndBehavioralTest` | `03-behavioral.txt`, `06-q18-compile-error.txt` |
| 19 | Decorator vs Proxy | `structural/Storage.java` | `StructuralTest` | `04-structural.txt` |
| 20 | JDK dynamic proxy | `structural/Greeter.java` | `StructuralTest` | `04-structural.txt` |
| 21 | Adapter vs Facade | `structural/Adapters.java` | `StructuralTest` | `04-structural.txt` |
| 22 | Composite | `structural/FileNode.java` | `StructuralTest` | `04-structural.txt` |
| 23 | Flyweight | `structural/GlyphFactory.java` | `StructuralTest` | `04-structural.txt` |
| 24 | Factory in Spring: `FactoryBean` | `spring/Beans.java` | `SpringPatternsTest` | `05-spring.txt` |
| 25 | Proxy in Spring: JDK vs CGLIB | `spring/Shared.java` | `SpringPatternsTest` | `05-spring.txt` |
| 26 | Proxy traps: self-invocation, final methods | `spring/Shared.java` | `SpringPatternsTest` | `05-spring.txt` |
| 27 | Observer in Spring: events | `spring/Beans.java` | `SpringPatternsTest` | `05-spring.txt` |
| 28 | Template/callback: `TransactionTemplate` | (test only) | `SpringPatternsTest` | `05-spring.txt` |
| 29 | Strategy by injection | `spring/Beans.java` | `SpringPatternsTest` | `05-spring.txt` |
| 30 | Prototype inside a singleton | `spring/Beans.java` | `SpringPatternsTest` | `05-spring.txt` |
## What is NOT proven here
Q4: the missing-`volatile` failure is allowed by the Java Memory Model but is practically unobservable on x86, so the test
asserts the modifier (`volatile` present in `DclSingleton`, absent in `DclWithoutVolatile`) rather than reproducing a torn read.
Q2 relies on a 20 ms sleep inside the constructor to make the race reliable; the exact number of extra instances varies per run,
so the test only asserts "more than one".
+2
View File
@@ -0,0 +1,2 @@
openjdk version "25.0.5" 2026-10-20 LTS
tests: 31
+13
View File
@@ -0,0 +1,13 @@
Q1 eager: getInstance() == getInstance() -> true
Q2 naive lazy, 8 threads: distinct instances=more than 1 constructor runs=8
Q3 synchronized, 8 threads: distinct instances=1 constructor runs=1
Q4 DCL with volatile, 8 threads: distinct instances=1 constructor runs=1
Q4 DclSingleton.instance volatile? true
Q4 DclWithoutVolatile.instance volatile? false
Q5 holder before first getInstance(): constructor runs=0
Q5 holder after two getInstance() calls: constructor runs=1 same=true
Q6 serialisation round trip keeps the same instance? classic=false hardened(readResolve)=true enum=true
Q6 reflection newInstance() on classic private constructor returns the singleton? false
Q6 reflection on hardened constructor: InvocationTargetException caused by java.lang.IllegalStateException: already initialised
Q6 reflection on enum constructor: java.lang.IllegalArgumentException: Cannot reflectively create enum objects
Q7 Spring singleton scope: same bean twice from one context? true; same bean across two contexts? false
+10
View File
@@ -0,0 +1,10 @@
Q8 simple factory: button("dark") -> [ dark button ]
Q8 factory method: new DarkDialog().show() -> dialog with [ dark button ]
Q9 LIGHT factory -> button=light checkbox=light
Q9 DARK factory -> button=dark checkbox=dark
Q10 builder defaults: method=GET timeout=30; headers map mutable? false
Q10 build() without url -> NullPointerException: url is required
Q10 JDK HttpRequest.newBuilder(uri).build() default method=GET
Q11 original tags after both copies were edited: [java, added-via-shallow]
Q11 shallow.tags() == original.tags()? true; deep.tags() == original.tags()? false
Q11 ArrayList.clone() shares elements: original.get(0) is now "a!"
+17
View File
@@ -0,0 +1,17 @@
Q12 state: advance() x3 -> NEW -> PAID -> SHIPPED -> DELIVERED
Q12 state: advance() on DELIVERED -> IllegalStateException: already delivered
Q12 strategy: total(1000) with NONE=1000.0 then FESTIVAL=900.0
Q13 sort by length (Comparator strategy): [fig, pear, banana]
Q13 sort reversed alphabetical: [pear, fig, banana]
Q13 inline lambda strategy: total(200)=150.0
Q14 publishNaive, 2 listeners, first unsubscribes itself: delivered to [first] (no exception, second was skipped)
Q14 publishNaive, 3 listeners, first unsubscribes itself -> ConcurrentModificationException
Q14 publishSafe, two events, self-unsubscribing listener: [once, other, other]
Q14 java.util.Observable @Deprecated(since="9")
Q15 template method run(): [open, sales rows, close]
Q15 strategy version runWith(): [open, ad-hoc rows, close]
Q15 run() is final (subclasses cannot reorder the steps): true
Q16 valid request -> 200 ok via [auth, rate-limit, controller]
Q16 no token -> 401 via [auth]
Q17 after two commands: "Hello, world"; undo -> "Hello"; undo -> ""
Q18 2 + 3 * 4 via sealed switch=14 via Visitor=14
+12
View File
@@ -0,0 +1,12 @@
Q19 LazyProxy: RealStorage instances before first read=0, after two reads=1
Q19 ProtectionProxy: read("secret-1") -> SecurityException: denied: secret-1
Q19 Decorators stacked Logging(Caching(Real)): read("k") twice -> log=[read k, read k]
Q20 proxy.greet("Ankur") -> hello Ankur; intercepted=[greet]
Q20 Proxy.isProxyClass(proxy.getClass())=true; proxy instanceof Greeter.Impl=false
Q20 newProxyInstance with a class instead of an interface -> IllegalArgumentException (... is not an interface)
Q21 adapter: 98.6F read through CelsiusSensor -> 37.0 C
Q21 facade: watchMovie() -> [lights dimmed, projector on, surround sound on]
Q21 JDK adapter InputStreamReader(InputStream) first char: h
Q22 size() of a leaf=10, of sub dir=25, of root=35
Q23 "hello world" has 11 characters but the pool holds 8 Glyph objects
Q23 Integer.valueOf: 127==127 -> true, 128==128 -> false
+18
View File
@@ -0,0 +1,18 @@
Q24 getBean("connection") -> Connection
Q24 getBean("&connection") -> ConnectionFactoryBean
Q24 ApplicationContext is itself a BeanFactory? true
Q25 target with interface, defaults -> jdkDynamicProxy=true cglib=false
Q25 target with interface, proxyTargetClass -> jdkDynamicProxy=false cglib=true
Q25 target with no interface -> jdkDynamicProxy=false cglib=true
Q25 JDK proxy instanceof GreeterImpl? false; CGLIB proxy instanceof GreeterImpl? true
Q26 proxy.outer() returned 2; advice saw [outer] (inner() ran, but was never intercepted)
Q26 proxy.inner() called from outside; advice saw [inner]
Q26 final method on a CGLIB proxy: finalMethod() -> "name=null", advice saw []
Q27 publishEvent(OrderPlaced): listener ran on the caller's thread? true
Q28 TransactionTemplate success path: [begin, work, commit] returned done
Q28 TransactionTemplate when the callback throws: [begin, work, rollback]
Q29 Map<String, Payment> injected with keys [card, upi]
Q29 pay("upi", 500) -> upi:500
Q30 getBean(Ticket) twice from the context: same instance? false
Q30 prototype injected into a singleton: same Ticket on every call? true
Q30 prototype fetched via ObjectProvider: same Ticket on every call? false
@@ -0,0 +1,4 @@
Q18Broken.java:9: error: the switch expression does not cover all possible input values
return switch (e) {
^
1 error
+58
View File
@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- Self-contained module. The rest of this repository is plain javac; this module needs
Maven only because its tests prove claims about Spring and use JUnit. -->
<groupId>com.ankurm.patterns</groupId>
<artifactId>interview</artifactId>
<version>1.0.0</version>
<name>Design patterns interview questions</name>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>7.0.10</spring.version>
<junit.version>6.1.3</junit.version>
<assertj.version>3.27.7</assertj.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
<configuration>
<argLine>-Xmx512m</argLine>
</configuration>
</plugin>
</plugins>
</build>
</project>
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Regenerates every file under output/ : the tests write 01-05, the compiler writes 06.
set -euo pipefail
cd "$(dirname "$0")/.."
rm -f output/*.txt
mvn -B -q test
javac --release 25 -d /tmp/q18-broken src/broken/Q18Broken.java 2> output/06-q18-compile-error.txt || true
# keep only the compiler's own diagnostic, not the JAVA_TOOL_OPTIONS banner or the file path prefix
sed -i -e '/^Picked up JAVA_TOOL_OPTIONS/d' -e 's#^src/broken/##' output/06-q18-compile-error.txt
{
java -version 2>&1 | grep -v '^Picked up' | head -1
echo "tests: $(grep -h -o 'tests="[0-9]*"' target/surefire-reports/TEST-*.xml | tr -dc '0-9\n' | paste -sd+ | bc)"
} > output/00-environment.txt
ls output
+15
View File
@@ -0,0 +1,15 @@
/** Q18: deliberately does not compile. A switch over a sealed type must cover every permitted subtype. */
public class Q18Broken {
sealed interface Expr permits Num, Add, Neg { }
record Num(int value) implements Expr { }
record Add(Expr left, Expr right) implements Expr { }
record Neg(Expr inner) implements Expr { } // the newly added subtype
static int eval(Expr e) {
return switch (e) {
case Num n -> n.value();
case Add a -> eval(a.left()) + eval(a.right());
// no case for Neg and no default
};
}
}
@@ -0,0 +1,18 @@
package interview.behavioral;
import java.util.function.DoubleUnaryOperator;
/** Q12/Q13: Strategy. The CLIENT picks the algorithm; it does not change by itself. Java 8+ makes it a lambda. */
public final class Checkout {
@FunctionalInterface
public interface DiscountStrategy extends DoubleUnaryOperator { }
public static final DiscountStrategy NONE = amount -> amount;
public static final DiscountStrategy FESTIVAL = amount -> amount * 0.90;
private DiscountStrategy strategy = NONE;
public void use(DiscountStrategy s) { this.strategy = s; }
public double total(double amount) { return strategy.applyAsDouble(amount); }
}
@@ -0,0 +1,33 @@
package interview.behavioral;
import java.util.ArrayDeque;
import java.util.Deque;
/** Q17: Command. Each action is an object that knows how to do and undo itself. */
public final class Editor {
public interface Command {
void execute();
void undo();
}
private final StringBuilder text = new StringBuilder();
private final Deque<Command> history = new ArrayDeque<>();
public Command append(String s) {
return new Command() {
public void execute() { text.append(s); }
public void undo() { text.setLength(text.length() - s.length()); }
};
}
public void run(Command c) {
c.execute();
history.push(c);
}
public void undo() {
if (!history.isEmpty()) history.pop().undo();
}
public String text() { return text.toString(); }
}
@@ -0,0 +1,25 @@
package interview.behavioral;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/** Q14: Observer, two versions: one that breaks when a listener unsubscribes mid-notification, and a safe one. */
public final class EventSource {
private final List<Consumer<String>> listeners = new ArrayList<>();
public void subscribe(Consumer<String> l) { listeners.add(l); }
public void unsubscribe(Consumer<String> l) { listeners.remove(l); }
/** Naive: iterates the live list. A listener that unsubscribes itself either skips the next listener silently or throws ConcurrentModificationException,
* depending on how many listeners there are. */
public void publishNaive(String event) {
for (Consumer<String> l : listeners) l.accept(event);
}
/** Safe: iterates a snapshot, so listeners may subscribe or unsubscribe while being notified. */
public void publishSafe(String event) {
for (Consumer<String> l : List.copyOf(listeners)) l.accept(event);
}
}
@@ -0,0 +1,32 @@
package interview.behavioral;
/** Q18: a closed set of types. With a sealed interface, plain switch replaces the Visitor pattern. */
public sealed interface Expr permits Expr.Num, Expr.Add, Expr.Mul {
record Num(int value) implements Expr {
public <R> R accept(Visitor<R> v) { return v.visitNum(this); }
}
record Add(Expr left, Expr right) implements Expr {
public <R> R accept(Visitor<R> v) { return v.visitAdd(this); }
}
record Mul(Expr left, Expr right) implements Expr {
public <R> R accept(Visitor<R> v) { return v.visitMul(this); }
}
/** Classic GoF visitor: double dispatch through accept(). */
interface Visitor<R> {
R visitNum(Num n);
R visitAdd(Add a);
R visitMul(Mul m);
}
<R> R accept(Visitor<R> v);
/** The modern alternative: no Visitor interface, no accept(); the compiler checks exhaustiveness. */
static int eval(Expr e) {
return switch (e) {
case Num n -> n.value();
case Add a -> eval(a.left()) + eval(a.right());
case Mul m -> eval(m.left()) * eval(m.right());
};
}
}
@@ -0,0 +1,32 @@
package interview.behavioral;
/** Q12: State. The OBJECT decides what happens next; the client never picks the state. */
public final class Order {
interface State {
State next();
String name();
}
private static final State DELIVERED = new State() {
public State next() { throw new IllegalStateException("already delivered"); }
public String name() { return "DELIVERED"; }
};
private static final State SHIPPED = new State() {
public State next() { return DELIVERED; }
public String name() { return "SHIPPED"; }
};
private static final State PAID = new State() {
public State next() { return SHIPPED; }
public String name() { return "PAID"; }
};
private static final State NEW = new State() {
public State next() { return PAID; }
public String name() { return "NEW"; }
};
private State state = NEW;
public void advance() { state = state.next(); }
public String state() { return state.name(); }
}
@@ -0,0 +1,31 @@
package interview.behavioral;
import java.util.ArrayList;
import java.util.List;
/** Q16: Chain of Responsibility. Each handler either stops the request or passes it on. */
public final class Pipeline {
public interface Handler {
/** @return null to continue, or a response to stop the chain. */
String handle(String request);
}
private final List<Handler> handlers = new ArrayList<>();
public final List<String> trace = new ArrayList<>();
public Pipeline add(String name, Handler h) {
handlers.add(request -> {
trace.add(name);
return h.handle(request);
});
return this;
}
public String process(String request) {
for (Handler h : handlers) {
String response = h.handle(request);
if (response != null) return response;
}
return "no handler";
}
}
@@ -0,0 +1,28 @@
package interview.behavioral;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
/** Q15: Template Method (inheritance) next to the Strategy version of the same job (composition). */
public abstract class ReportJob {
protected final List<String> log = new ArrayList<>();
protected abstract String body(); // the step subclasses fill in
public final List<String> run() { // the template: final, so the order cannot change
log.add("open");
log.add(body());
log.add("close");
return log;
}
public static final class SalesReport extends ReportJob {
@Override protected String body() { return "sales rows"; }
}
/** Same skeleton, but the varying step is injected: no subclass needed, can change at runtime. */
public static List<String> runWith(Supplier<String> body) {
return List.of("open", body.get(), "close");
}
}
@@ -0,0 +1,8 @@
package interview.creational;
/** Shared product type for Q8 and Q9. */
public interface Button {
String theme();
String render();
}
@@ -0,0 +1,18 @@
package interview.creational;
/** Q8: Factory Method. The base class has the algorithm; a SUBCLASS decides which product to create. */
public abstract class Dialog {
protected abstract Button createButton(); // the factory method
public final String show() {
return "dialog with " + createButton().render();
}
public static final class LightDialog extends Dialog {
@Override protected Button createButton() { return new SimpleFactory.LightButton(); }
}
public static final class DarkDialog extends Dialog {
@Override protected Button createButton() { return new SimpleFactory.DarkButton(); }
}
}
@@ -0,0 +1,24 @@
package interview.creational;
import java.util.ArrayList;
import java.util.List;
/** Q11: Prototype. Copying an object instead of building from scratch, and the shallow/deep trap. */
public final class Doc {
private final String title;
private final List<String> tags;
public Doc(String title, List<String> tags) {
this.title = title;
this.tags = tags;
}
public String title() { return title; }
public List<String> tags() { return tags; }
/** Shallow copy: the new Doc shares the SAME list object. */
public Doc shallowCopy() { return new Doc(title, tags); }
/** Deep copy: the new Doc gets its own list. */
public Doc deepCopy() { return new Doc(title, new ArrayList<>(tags)); }
}
@@ -0,0 +1,22 @@
package interview.creational;
/** Q9: Abstract Factory. One object creates a whole FAMILY of related products. */
public interface GuiFactory {
Button createButton();
Checkbox createCheckbox();
interface Checkbox {
String theme();
}
GuiFactory LIGHT = new GuiFactory() {
public Button createButton() { return new SimpleFactory.LightButton(); }
public Checkbox createCheckbox() { return () -> "light"; }
};
GuiFactory DARK = new GuiFactory() {
public Button createButton() { return new SimpleFactory.DarkButton(); }
public Checkbox createCheckbox() { return () -> "dark"; }
};
}
@@ -0,0 +1,45 @@
package interview.creational;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
/** Q10: Builder. Step-by-step construction with validation, producing an immutable object. */
public final class HttpCall {
private final String method;
private final String url;
private final Map<String, String> headers;
private final int timeoutSeconds;
private HttpCall(Builder b) {
this.method = b.method;
this.url = b.url;
this.headers = Map.copyOf(b.headers);
this.timeoutSeconds = b.timeoutSeconds;
}
public String method() { return method; }
public String url() { return url; }
public Map<String, String> headers() { return headers; }
public int timeoutSeconds() { return timeoutSeconds; }
public static Builder builder() { return new Builder(); }
public static final class Builder {
private String method = "GET";
private String url;
private final Map<String, String> headers = new LinkedHashMap<>();
private int timeoutSeconds = 30;
public Builder method(String m) { this.method = m; return this; }
public Builder url(String u) { this.url = u; return this; }
public Builder header(String k, String v) { this.headers.put(k, v); return this; }
public Builder timeoutSeconds(int s) { this.timeoutSeconds = s; return this; }
public HttpCall build() {
Objects.requireNonNull(url, "url is required"); // validated once, in one place
if (timeoutSeconds <= 0) throw new IllegalArgumentException("timeout must be positive");
return new HttpCall(this);
}
}
}
@@ -0,0 +1,24 @@
package interview.creational;
/** Q8: a simple (static) factory. Not a GoF pattern: one method decides, no subclassing involved. */
public final class SimpleFactory {
private SimpleFactory() { }
public static Button button(String theme) {
return switch (theme) {
case "light" -> new LightButton();
case "dark" -> new DarkButton();
default -> throw new IllegalArgumentException("unknown theme: " + theme);
};
}
public record LightButton() implements Button {
public String theme() { return "light"; }
public String render() { return "[ light button ]"; }
}
public record DarkButton() implements Button {
public String theme() { return "dark"; }
public String render() { return "[ dark button ]"; }
}
}
@@ -0,0 +1,12 @@
package interview.singleton;
import java.io.Serializable;
/** Q6: a classic eager singleton that is serialisable and has NO protection: both attacks succeed. */
public final class ClassicSingleton implements Serializable {
private static final ClassicSingleton INSTANCE = new ClassicSingleton();
private ClassicSingleton() { }
public static ClassicSingleton getInstance() { return INSTANCE; }
}
@@ -0,0 +1,34 @@
package interview.singleton;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Q4: double-checked locking. {@code volatile} is what makes the unsynchronised first read safe:
* without it the JMM allows another thread to see a non-null reference to a not-yet-constructed object.
*/
public final class DclSingleton {
private static volatile DclSingleton instance;
private static final AtomicInteger CREATED = new AtomicInteger();
private DclSingleton() {
CREATED.incrementAndGet();
try { Thread.sleep(20); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
public static DclSingleton getInstance() {
DclSingleton local = instance; // one volatile read on the fast path
if (local == null) {
synchronized (DclSingleton.class) {
local = instance;
if (local == null) {
instance = local = new DclSingleton();
}
}
}
return local;
}
public static int created() { return CREATED.get(); }
public static void reset() { synchronized (DclSingleton.class) { instance = null; CREATED.set(0); } }
}
@@ -0,0 +1,25 @@
package interview.singleton;
/**
* Q4: the BROKEN variant, kept only so a test can show the field is not volatile.
* On x86 you will almost never see the failure; the JMM still permits it (and ARM can show it).
*/
public final class DclWithoutVolatile {
private static DclWithoutVolatile instance; // <-- missing volatile
private int state = 42;
private DclWithoutVolatile() { }
public static DclWithoutVolatile getInstance() {
if (instance == null) {
synchronized (DclWithoutVolatile.class) {
if (instance == null) {
instance = new DclWithoutVolatile(); // allocate, publish reference, run constructor: any order
}
}
}
return instance;
}
public int state() { return state; }
}
@@ -0,0 +1,10 @@
package interview.singleton;
/** Q1: eager initialisation. The JVM guarantees class initialisation runs once, thread-safely. */
public final class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() { }
public static EagerSingleton getInstance() { return INSTANCE; }
}
@@ -0,0 +1,10 @@
package interview.singleton;
/** Q6: the enum singleton. The JVM blocks reflective construction and serialises it by name. */
public enum EnumSingleton {
INSTANCE;
private int counter;
public int next() { return ++counter; }
}
@@ -0,0 +1,18 @@
package interview.singleton;
import java.io.Serializable;
/** Q6: the same class with readResolve (serialisation) and a constructor guard (reflection). */
public final class HardenedSingleton implements Serializable {
private static final HardenedSingleton INSTANCE = new HardenedSingleton();
private HardenedSingleton() {
if (INSTANCE != null) {
throw new IllegalStateException("already initialised");
}
}
public static HardenedSingleton getInstance() { return INSTANCE; }
private Object readResolve() { return INSTANCE; }
}
@@ -0,0 +1,18 @@
package interview.singleton;
import java.util.concurrent.atomic.AtomicInteger;
/** Q5: initialisation-on-demand holder. Lazy and lock-free because class init is lazy and thread-safe. */
public final class HolderSingleton {
private static final AtomicInteger CREATED = new AtomicInteger();
private HolderSingleton() { CREATED.incrementAndGet(); }
private static final class Holder {
static final HolderSingleton INSTANCE = new HolderSingleton();
}
public static HolderSingleton getInstance() { return Holder.INSTANCE; }
public static int created() { return CREATED.get(); }
}
@@ -0,0 +1,25 @@
package interview.singleton;
import java.util.concurrent.atomic.AtomicInteger;
/** Q2: lazy but NOT thread-safe. The sleep widens the race window so the bug is reproducible. */
public final class NaiveLazySingleton {
private static NaiveLazySingleton instance;
private static final AtomicInteger CREATED = new AtomicInteger();
private NaiveLazySingleton() {
CREATED.incrementAndGet();
try { Thread.sleep(20); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
public static NaiveLazySingleton getInstance() {
if (instance == null) { // check-then-act: two threads can both see null
instance = new NaiveLazySingleton();
}
return instance;
}
public static int created() { return CREATED.get(); }
public static void reset() { instance = null; CREATED.set(0); }
}
@@ -0,0 +1,25 @@
package interview.singleton;
import java.util.concurrent.atomic.AtomicInteger;
/** Q3: correct, but every call pays for the lock, even after initialisation. */
public final class SynchronizedSingleton {
private static SynchronizedSingleton instance;
private static final AtomicInteger CREATED = new AtomicInteger();
private SynchronizedSingleton() {
CREATED.incrementAndGet();
try { Thread.sleep(20); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
public static synchronized SynchronizedSingleton getInstance() {
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
public static int created() { return CREATED.get(); }
public static void reset() { synchronized (SynchronizedSingleton.class) { instance = null; CREATED.set(0); } }
}
@@ -0,0 +1,76 @@
package interview.spring;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.annotation.Scope;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/** Beans for Q24 (FactoryBean), Q27 (events), Q29 (strategy by injection) and Q30 (prototype in singleton). */
public final class Beans {
private Beans() { }
// Q24
public record Connection(String url) { }
public static class ConnectionFactoryBean implements FactoryBean<Connection> {
public Connection getObject() { return new Connection("jdbc:demo"); }
public Class<?> getObjectType() { return Connection.class; }
}
// Q27
public static class OrderPlaced extends ApplicationEvent {
public OrderPlaced(Object source) { super(source); }
}
public static class Listener {
public final List<String> received = new ArrayList<>();
@EventListener
public void on(OrderPlaced e) { received.add(Thread.currentThread().getName()); }
}
// Q29
@Component("card")
public static class CardPayment implements Shared.Payment {
public String pay(int amount) { return "card:" + amount; }
}
@Component("upi")
public static class UpiPayment implements Shared.Payment {
public String pay(int amount) { return "upi:" + amount; }
}
public static class PaymentService {
private final Map<String, Shared.Payment> byName;
@Autowired
public PaymentService(Map<String, Shared.Payment> byName) { this.byName = byName; }
public String pay(String method, int amount) { return byName.get(method).pay(amount); }
public java.util.Set<String> methods() { return new java.util.TreeSet<>(byName.keySet()); }
}
// Q30
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public static class Ticket { }
public static class StaleHolder {
private final Ticket ticket;
@Autowired public StaleHolder(Ticket ticket) { this.ticket = ticket; }
public Ticket ticket() { return ticket; }
}
public static class FreshHolder {
private final ObjectProvider<Ticket> tickets;
@Autowired public FreshHolder(ObjectProvider<Ticket> tickets) { this.tickets = tickets; }
public Ticket ticket() { return tickets.getObject(); }
}
}
@@ -0,0 +1,6 @@
package interview.spring;
/** Q7 / Q30 helper beans. A plain class: Spring decides whether there is one of it, not the class itself. */
public class ScopeDemo {
public static class Service { }
}
@@ -0,0 +1,21 @@
package interview.spring;
/** Types used by the Spring questions (Q24-Q30). */
public final class Shared {
private Shared() { }
public interface Payment { String pay(int amount); }
public static class Calculator {
public int outer() { return inner() + 1; } // self-invocation: this.inner(), not proxy.inner()
public int inner() { return 1; }
public final String finalMethod() { return "name=" + name; }
private String name = "real";
}
public interface Greeter { String hello(); }
public static class GreeterImpl implements Greeter {
public String hello() { return "hello"; }
}
}
@@ -0,0 +1,37 @@
package interview.structural;
/** Q21: Adapter changes an interface; Facade simplifies a subsystem. */
public final class Adapters {
private Adapters() { }
/** What the client wants. */
public interface CelsiusSensor { double celsius(); }
/** What we have and cannot change. */
public static final class LegacyFahrenheitSensor {
public double readFahrenheit() { return 98.6; }
}
/** Adapter: same behaviour, different interface. */
public static final class FahrenheitAdapter implements CelsiusSensor {
private final LegacyFahrenheitSensor legacy;
public FahrenheitAdapter(LegacyFahrenheitSensor legacy) { this.legacy = legacy; }
public double celsius() { return (legacy.readFahrenheit() - 32) * 5 / 9; }
}
/** Three subsystem classes... */
public static final class Lights { public String dim() { return "lights dimmed"; } }
public static final class Projector { public String on() { return "projector on"; } }
public static final class Sound { public String surround() { return "surround sound on"; } }
/** ...and a Facade: ONE new, simpler interface; the subsystem classes keep their own interfaces. */
public static final class HomeTheater {
private final Lights lights = new Lights();
private final Projector projector = new Projector();
private final Sound sound = new Sound();
public java.util.List<String> watchMovie() {
return java.util.List.of(lights.dim(), projector.on(), sound.surround());
}
}
}
@@ -0,0 +1,14 @@
package interview.structural;
import java.util.List;
/** Q22: Composite. Leaves and containers share one interface, so callers treat both the same. */
public sealed interface FileNode permits FileNode.File, FileNode.Dir {
long size();
record File(String name, long size) implements FileNode { }
record Dir(String name, List<FileNode> children) implements FileNode {
public long size() { return children.stream().mapToLong(FileNode::size).sum(); }
}
}
@@ -0,0 +1,17 @@
package interview.structural;
import java.util.HashMap;
import java.util.Map;
/** Q23: Flyweight. Share the immutable, intrinsic state; pass the extrinsic state (position) in per call. */
public final class GlyphFactory {
public record Glyph(char symbol) { // intrinsic, shared
public String draw(int x, int y) { return symbol + "@" + x + "," + y; } // x,y are extrinsic
}
private final Map<Character, Glyph> pool = new HashMap<>();
public Glyph get(char c) { return pool.computeIfAbsent(c, Glyph::new); }
public int poolSize() { return pool.size(); }
}
@@ -0,0 +1,10 @@
package interview.structural;
/** Q20 helper: an interface for java.lang.reflect.Proxy to implement. */
public interface Greeter {
String greet(String name);
final class Impl implements Greeter {
public String greet(String name) { return "hello " + name; }
}
}
@@ -0,0 +1,64 @@
package interview.structural;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Q19: Decorator vs Proxy. Same interface, different intent. */
public interface Storage {
String read(String key);
/** The real, expensive object. */
final class RealStorage implements Storage {
public static int instances;
public RealStorage() { instances++; }
public String read(String key) { return "value-of-" + key; }
}
/**
* Proxy: controls ACCESS. It owns the decision about when the real object exists; the client never passes it in.
*/
final class LazyProxy implements Storage {
private RealStorage real;
public String read(String key) {
if (real == null) real = new RealStorage();
return real.read(key);
}
}
/** Proxy (protection flavour): refuses some calls. */
final class ProtectionProxy implements Storage {
private final Storage target;
public ProtectionProxy(Storage target) { this.target = target; }
public String read(String key) {
if (key.startsWith("secret")) throw new SecurityException("denied: " + key);
return target.read(key);
}
}
/** Decorator: adds BEHAVIOUR. The client composes it around any Storage, including other decorators. */
final class Caching implements Storage {
private final Storage inner;
private final Map<String, String> cache = new HashMap<>();
public Caching(Storage inner) { this.inner = inner; }
public String read(String key) { return cache.computeIfAbsent(key, inner::read); }
}
/** Decorator: adds behaviour (logging) around any Storage. */
final class Logging implements Storage {
private final Storage inner;
private final List<String> log;
public Logging(Storage inner, List<String> log) { this.inner = inner; this.log = log; }
public String read(String key) {
log.add("read " + key);
return inner.read(key);
}
}
}
@@ -0,0 +1,197 @@
package interview;
import static org.assertj.core.api.Assertions.*;
import interview.behavioral.*;
import interview.creational.*;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.http.HttpRequest;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.Observable;
import java.util.function.Consumer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
/** Questions 8-18. Writes output/02-creational.txt and output/03-behavioral.txt. */
class CreationalAndBehavioralTest {
static final Transcript C = new Transcript("02-creational");
static final Transcript B = new Transcript("03-behavioral");
@AfterAll static void write() { C.write(); B.write(); }
@Test void q8_simpleFactoryVsFactoryMethod() {
assertThat(SimpleFactory.button("dark").render()).isEqualTo("[ dark button ]");
assertThatThrownBy(() -> SimpleFactory.button("neon")).hasMessage("unknown theme: neon");
assertThat(new Dialog.LightDialog().show()).isEqualTo("dialog with [ light button ]");
assertThat(new Dialog.DarkDialog().show()).isEqualTo("dialog with [ dark button ]");
C.line("Q8 simple factory: button(\"dark\") -> %s", SimpleFactory.button("dark").render());
C.line("Q8 factory method: new DarkDialog().show() -> %s", new Dialog.DarkDialog().show());
}
@Test void q9_abstractFactoryKeepsFamiliesConsistent() {
for (GuiFactory f : List.of(GuiFactory.LIGHT, GuiFactory.DARK)) {
assertThat(f.createButton().theme()).isEqualTo(f.createCheckbox().theme());
}
C.line("Q9 LIGHT factory -> button=%s checkbox=%s", GuiFactory.LIGHT.createButton().theme(), GuiFactory.LIGHT.createCheckbox().theme());
C.line("Q9 DARK factory -> button=%s checkbox=%s", GuiFactory.DARK.createButton().theme(), GuiFactory.DARK.createCheckbox().theme());
}
@Test void q10_builderValidatesAndIsImmutable() {
HttpCall call = HttpCall.builder().url("https://example.com").header("Accept", "application/json").build();
assertThat(call.method()).isEqualTo("GET");
assertThat(call.timeoutSeconds()).isEqualTo(30);
assertThatThrownBy(() -> call.headers().put("x", "y")).isInstanceOf(UnsupportedOperationException.class);
assertThatThrownBy(() -> HttpCall.builder().build()).isInstanceOf(NullPointerException.class).hasMessage("url is required");
// the JDK's own builder: build() is where validation happens
HttpRequest jdk = HttpRequest.newBuilder(URI.create("https://example.com")).header("Accept", "text/plain").build();
assertThat(jdk.method()).isEqualTo("GET");
C.line("Q10 builder defaults: method=%s timeout=%d; headers map mutable? false", call.method(), call.timeoutSeconds());
C.line("Q10 build() without url -> NullPointerException: url is required");
C.line("Q10 JDK HttpRequest.newBuilder(uri).build() default method=%s", jdk.method());
}
@Test void q11_shallowCopySharesTheListDeepCopyDoesNot() {
Doc original = new Doc("spec", new ArrayList<>(List.of("java")));
Doc shallow = original.shallowCopy();
Doc deep = original.deepCopy();
shallow.tags().add("added-via-shallow");
deep.tags().add("added-via-deep");
C.line("Q11 original tags after both copies were edited: %s", original.tags());
C.line("Q11 shallow.tags() == original.tags()? %s; deep.tags() == original.tags()? %s",
shallow.tags() == original.tags(), deep.tags() == original.tags());
assertThat(original.tags()).containsExactly("java", "added-via-shallow");
assertThat(shallow.tags()).isSameAs(original.tags());
assertThat(deep.tags()).isNotSameAs(original.tags());
// ArrayList.clone() is itself shallow: the elements are shared
ArrayList<StringBuilder> list = new ArrayList<>(List.of(new StringBuilder("a")));
@SuppressWarnings("unchecked") ArrayList<StringBuilder> cloned = (ArrayList<StringBuilder>) list.clone();
cloned.get(0).append("!");
C.line("Q11 ArrayList.clone() shares elements: original.get(0) is now \"%s\"", list.get(0));
assertThat(list.get(0).toString()).isEqualTo("a!");
}
@Test void q12_stateMovesItselfStrategyIsChosenByTheClient() {
Order order = new Order();
List<String> seen = new ArrayList<>(List.of(order.state()));
for (int i = 0; i < 3; i++) { order.advance(); seen.add(order.state()); }
B.line("Q12 state: advance() x3 -> %s", String.join(" -> ", seen));
assertThatThrownBy(order::advance).isInstanceOf(IllegalStateException.class).hasMessage("already delivered");
B.line("Q12 state: advance() on DELIVERED -> IllegalStateException: already delivered");
Checkout checkout = new Checkout();
double before = checkout.total(1000);
checkout.use(Checkout.FESTIVAL); // only the client can change the strategy
double after = checkout.total(1000);
B.line("Q12 strategy: total(1000) with NONE=%.1f then FESTIVAL=%.1f", before, after);
assertThat(seen).containsExactly("NEW", "PAID", "SHIPPED", "DELIVERED");
assertThat(before).isEqualTo(1000.0);
assertThat(after).isEqualTo(900.0);
}
@Test void q13_strategyIsALambdaInModernJava() {
List<String> words = new ArrayList<>(List.of("pear", "fig", "banana"));
words.sort(java.util.Comparator.comparingInt(String::length)); // Comparator IS the strategy
B.line("Q13 sort by length (Comparator strategy): %s", words);
words.sort(java.util.Comparator.<String>naturalOrder().reversed());
B.line("Q13 sort reversed alphabetical: %s", words);
assertThat(words).containsExactly("pear", "fig", "banana");
Checkout c = new Checkout();
c.use(amount -> amount - 50); // a one-off strategy, no class declared
B.line("Q13 inline lambda strategy: total(200)=%.1f", c.total(200));
assertThat(c.total(200)).isEqualTo(150.0);
}
@Test void q14_observerUnsubscribeDuringNotify() {
// two listeners: the first removes itself. ArrayList's iterator ends early WITHOUT throwing: listener 2 is silently skipped
EventSource two = new EventSource();
List<String> twoGot = new ArrayList<>();
Consumer<String>[] first = new Consumer[1];
first[0] = e -> { twoGot.add("first"); two.unsubscribe(first[0]); };
two.subscribe(first[0]);
two.subscribe(e -> twoGot.add("second"));
two.publishNaive("x");
B.line("Q14 publishNaive, 2 listeners, first unsubscribes itself: delivered to %s (no exception, second was skipped)", twoGot);
assertThat(twoGot).containsExactly("first");
// three listeners: now the iterator's size check fails and ConcurrentModificationException is thrown
EventSource three = new EventSource();
Consumer<String>[] self = new Consumer[1];
self[0] = e -> three.unsubscribe(self[0]);
three.subscribe(self[0]);
three.subscribe(e -> { });
three.subscribe(e -> { });
assertThatThrownBy(() -> three.publishNaive("x")).isInstanceOf(ConcurrentModificationException.class);
B.line("Q14 publishNaive, 3 listeners, first unsubscribes itself -> ConcurrentModificationException");
EventSource safe = new EventSource();
List<String> got = new ArrayList<>();
Consumer<String>[] self2 = new Consumer[1];
self2[0] = e -> { got.add("once"); safe.unsubscribe(self2[0]); };
safe.subscribe(self2[0]);
safe.subscribe(e -> got.add("other"));
safe.publishSafe("x");
safe.publishSafe("y");
B.line("Q14 publishSafe, two events, self-unsubscribing listener: %s", got);
assertThat(got).containsExactly("once", "other", "other");
Deprecated d = Observable.class.getAnnotation(Deprecated.class);
B.line("Q14 java.util.Observable @Deprecated(since=\"%s\")", d.since());
assertThat(d.since()).isEqualTo("9");
}
@Test void q15_templateMethodVsStrategy() throws Exception {
ReportJob job = new ReportJob.SalesReport();
B.line("Q15 template method run(): %s", job.run());
B.line("Q15 strategy version runWith(): %s", ReportJob.runWith(() -> "ad-hoc rows"));
assertThat(job.run()).startsWith("open", "sales rows", "close");
assertThat(Modifier.isFinal(ReportJob.class.getMethod("run").getModifiers())).isTrue();
B.line("Q15 run() is final (subclasses cannot reorder the steps): true");
assertThat(ReportJob.runWith(() -> "ad-hoc rows")).containsExactly("open", "ad-hoc rows", "close");
}
@Test void q16_chainOfResponsibilityShortCircuits() {
Pipeline p = new Pipeline()
.add("auth", r -> r.startsWith("token:") ? null : "401")
.add("rate-limit", r -> r.endsWith("!") ? "429" : null)
.add("controller", r -> "200 ok");
String ok = p.process("token:abc");
List<String> okTrace = List.copyOf(p.trace);
p.trace.clear();
String denied = p.process("anonymous");
B.line("Q16 valid request -> %s via %s", ok, okTrace);
B.line("Q16 no token -> %s via %s", denied, p.trace);
assertThat(ok).isEqualTo("200 ok");
assertThat(okTrace).containsExactly("auth", "rate-limit", "controller");
assertThat(denied).isEqualTo("401");
assertThat(p.trace).containsExactly("auth");
}
@Test void q17_commandSupportsUndo() {
Editor ed = new Editor();
ed.run(ed.append("Hello"));
ed.run(ed.append(", world"));
String full = ed.text();
ed.undo();
String once = ed.text();
ed.undo();
B.line("Q17 after two commands: \"%s\"; undo -> \"%s\"; undo -> \"%s\"", full, once, ed.text());
assertThat(full).isEqualTo("Hello, world");
assertThat(once).isEqualTo("Hello");
assertThat(ed.text()).isEmpty();
}
@Test void q18_visitorAndSealedSwitchAgree() {
Expr e = new Expr.Add(new Expr.Num(2), new Expr.Mul(new Expr.Num(3), new Expr.Num(4)));
int viaSwitch = Expr.eval(e);
int viaVisitor = e.accept(new Expr.Visitor<Integer>() {
public Integer visitNum(Expr.Num n) { return n.value(); }
public Integer visitAdd(Expr.Add a) { return a.left().accept(this) + a.right().accept(this); }
public Integer visitMul(Expr.Mul m) { return m.left().accept(this) * m.right().accept(this); }
});
B.line("Q18 2 + 3 * 4 via sealed switch=%d via Visitor=%d", viaSwitch, viaVisitor);
assertThat(viaSwitch).isEqualTo(14).isEqualTo(viaVisitor);
}
}
@@ -0,0 +1,146 @@
package interview;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import interview.singleton.*;
import interview.spring.ScopeDemo;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.*;
import java.util.function.Supplier;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/** Questions 1-7. Every assertion also writes a line to output/01-singleton.txt. */
class SingletonInterviewTest {
static final Transcript T = new Transcript("01-singleton");
@AfterAll static void write() { T.write(); }
/** Runs the supplier on 8 threads released together and returns the distinct instances seen. */
static Set<Object> race(Supplier<Object> getter) throws Exception {
int threads = 8;
CyclicBarrier barrier = new CyclicBarrier(threads);
try (ExecutorService pool = Executors.newFixedThreadPool(threads)) {
List<Future<Object>> futures = new ArrayList<>();
for (int i = 0; i < threads; i++) {
futures.add(pool.submit(() -> { barrier.await(); return getter.get(); }));
}
Set<Object> seen = java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>());
for (Future<Object> f : futures) seen.add(f.get());
return seen;
}
}
@Test void q1_eagerSingletonIsOneInstance() {
assertThat(EagerSingleton.getInstance()).isSameAs(EagerSingleton.getInstance());
T.line("Q1 eager: getInstance() == getInstance() -> true");
}
@Test void q2_naiveLazyCreatesMoreThanOneUnderContention() throws Exception {
NaiveLazySingleton.reset();
Set<Object> seen = race(NaiveLazySingleton::getInstance);
T.line("Q2 naive lazy, 8 threads: distinct instances=%s constructor runs=%d",
seen.size() > 1 ? "more than 1" : "1", NaiveLazySingleton.created());
assertThat(seen.size()).isGreaterThan(1);
assertThat(NaiveLazySingleton.created()).isGreaterThan(1);
}
@Test void q3_synchronizedMethodIsCorrect() throws Exception {
SynchronizedSingleton.reset();
Set<Object> seen = race(SynchronizedSingleton::getInstance);
T.line("Q3 synchronized, 8 threads: distinct instances=%d constructor runs=%d",
seen.size(), SynchronizedSingleton.created());
assertThat(seen).hasSize(1);
assertThat(SynchronizedSingleton.created()).isEqualTo(1);
}
@Test void q4_dclIsCorrectAndItsFieldIsVolatile() throws Exception {
DclSingleton.reset();
Set<Object> seen = race(DclSingleton::getInstance);
boolean volatileOk = Modifier.isVolatile(DclSingleton.class.getDeclaredField("instance").getModifiers());
boolean volatileBroken = Modifier.isVolatile(DclWithoutVolatile.class.getDeclaredField("instance").getModifiers());
T.line("Q4 DCL with volatile, 8 threads: distinct instances=%d constructor runs=%d", seen.size(), DclSingleton.created());
T.line("Q4 DclSingleton.instance volatile? %s", volatileOk);
T.line("Q4 DclWithoutVolatile.instance volatile? %s", volatileBroken);
assertThat(seen).hasSize(1);
assertThat(volatileOk).isTrue();
assertThat(volatileBroken).isFalse();
}
@Test void q5_holderIsLazy() {
T.line("Q5 holder before first getInstance(): constructor runs=%d", HolderSingleton.created());
assertThat(HolderSingleton.created()).isZero();
HolderSingleton a = HolderSingleton.getInstance();
HolderSingleton b = HolderSingleton.getInstance();
T.line("Q5 holder after two getInstance() calls: constructor runs=%d same=%s", HolderSingleton.created(), a == b);
assertThat(HolderSingleton.created()).isEqualTo(1);
assertThat(a).isSameAs(b);
}
@SuppressWarnings("unchecked")
static <T> T roundTrip(T o) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream out = new ObjectOutputStream(bytes)) { out.writeObject(o); }
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
return (T) in.readObject();
}
}
@Test void q6_classicSingletonBreaksEnumAndHardenedSurvive() throws Exception {
// serialisation
ClassicSingleton copy = roundTrip(ClassicSingleton.getInstance());
boolean classicSer = copy == ClassicSingleton.getInstance();
boolean hardenedSer = roundTrip(HardenedSingleton.getInstance()) == HardenedSingleton.getInstance();
boolean enumSer = roundTrip(EnumSingleton.INSTANCE) == EnumSingleton.INSTANCE;
T.line("Q6 serialisation round trip keeps the same instance? classic=%s hardened(readResolve)=%s enum=%s",
classicSer, hardenedSer, enumSer);
assertThat(classicSer).isFalse();
assertThat(hardenedSer).isTrue();
assertThat(enumSer).isTrue();
// reflection
Constructor<ClassicSingleton> c = ClassicSingleton.class.getDeclaredConstructor();
c.setAccessible(true);
boolean classicRef = c.newInstance() == ClassicSingleton.getInstance();
T.line("Q6 reflection newInstance() on classic private constructor returns the singleton? %s", classicRef);
assertThat(classicRef).isFalse();
Constructor<HardenedSingleton> h = HardenedSingleton.class.getDeclaredConstructor();
h.setAccessible(true);
Throwable hardenedFailure = org.assertj.core.api.Assertions.catchThrowable(h::newInstance);
assertThat(hardenedFailure).isInstanceOf(InvocationTargetException.class).hasRootCauseMessage("already initialised");
T.line("Q6 reflection on hardened constructor: %s caused by %s", "InvocationTargetException", hardenedFailure.getCause());
Constructor<EnumSingleton> e = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
e.setAccessible(true);
Throwable enumFailure = org.assertj.core.api.Assertions.catchThrowable(() -> e.newInstance("COPY", 1));
T.line("Q6 reflection on enum constructor: %s", enumFailure);
assertThat(enumFailure).isInstanceOf(IllegalArgumentException.class)
.hasMessage("Cannot reflectively create enum objects");
}
@Test void q7_springSingletonIsPerContainerNotPerJvm() {
try (AnnotationConfigApplicationContext one = new AnnotationConfigApplicationContext();
AnnotationConfigApplicationContext two = new AnnotationConfigApplicationContext()) {
one.registerBean(ScopeDemo.Service.class);
two.registerBean(ScopeDemo.Service.class);
one.refresh();
two.refresh();
boolean sameInOne = one.getBean(ScopeDemo.Service.class) == one.getBean(ScopeDemo.Service.class);
boolean acrossContexts = one.getBean(ScopeDemo.Service.class) == two.getBean(ScopeDemo.Service.class);
T.line("Q7 Spring singleton scope: same bean twice from one context? %s; same bean across two contexts? %s",
sameInOne, acrossContexts);
assertThat(sameInOne).isTrue();
assertThat(acrossContexts).isFalse();
}
}
}
@@ -0,0 +1,144 @@
package interview;
import static org.assertj.core.api.Assertions.*;
import interview.spring.Beans;
import interview.spring.Shared;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.aopalliance.intercept.MethodInterceptor;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.SimpleTransactionStatus;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.transaction.PlatformTransactionManager;
/** Questions 24-30: where Spring uses each pattern. Writes output/05-spring.txt. */
class SpringPatternsTest {
static final Transcript T = new Transcript("05-spring");
@AfterAll static void write() { T.write(); }
@Test void q24_factoryBeanReturnsTheProductNotTheFactory() {
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) {
ctx.registerBean("connection", Beans.ConnectionFactoryBean.class);
ctx.refresh();
Object product = ctx.getBean("connection");
Object factory = ctx.getBean("&connection");
T.line("Q24 getBean(\"connection\") -> %s", product.getClass().getSimpleName());
T.line("Q24 getBean(\"&connection\") -> %s", factory.getClass().getSimpleName());
T.line("Q24 ApplicationContext is itself a BeanFactory? %s", ctx instanceof org.springframework.beans.factory.BeanFactory);
assertThat(product).isInstanceOf(Beans.Connection.class);
assertThat(factory).isInstanceOf(Beans.ConnectionFactoryBean.class);
}
}
@Test void q25_jdkProxyVsCglibProxy() {
ProxyFactory withInterface = new ProxyFactory(new Shared.GreeterImpl());
Object jdk = withInterface.getProxy();
ProxyFactory forced = new ProxyFactory(new Shared.GreeterImpl());
forced.setProxyTargetClass(true);
Object cglib = forced.getProxy();
Object noInterface = new ProxyFactory(new Shared.Calculator()).getProxy();
T.line("Q25 target with interface, defaults -> jdkDynamicProxy=%s cglib=%s",
AopUtils.isJdkDynamicProxy(jdk), AopUtils.isCglibProxy(jdk));
T.line("Q25 target with interface, proxyTargetClass -> jdkDynamicProxy=%s cglib=%s",
AopUtils.isJdkDynamicProxy(cglib), AopUtils.isCglibProxy(cglib));
T.line("Q25 target with no interface -> jdkDynamicProxy=%s cglib=%s",
AopUtils.isJdkDynamicProxy(noInterface), AopUtils.isCglibProxy(noInterface));
T.line("Q25 JDK proxy instanceof GreeterImpl? %s; CGLIB proxy instanceof GreeterImpl? %s",
jdk instanceof Shared.GreeterImpl, cglib instanceof Shared.GreeterImpl);
assertThat(AopUtils.isJdkDynamicProxy(jdk)).isTrue();
assertThat(AopUtils.isCglibProxy(cglib)).isTrue();
assertThat(AopUtils.isCglibProxy(noInterface)).isTrue();
assertThat(jdk).isNotInstanceOf(Shared.GreeterImpl.class);
assertThat(cglib).isInstanceOf(Shared.GreeterImpl.class);
}
@Test void q26_selfInvocationBypassesTheProxy() {
List<String> intercepted = new ArrayList<>();
ProxyFactory pf = new ProxyFactory(new Shared.Calculator());
pf.addAdvice((MethodInterceptor) inv -> { intercepted.add(inv.getMethod().getName()); return inv.proceed(); });
Shared.Calculator proxy = (Shared.Calculator) pf.getProxy();
int result = proxy.outer();
T.line("Q26 proxy.outer() returned %d; advice saw %s (inner() ran, but was never intercepted)", result, intercepted);
assertThat(result).isEqualTo(2);
assertThat(intercepted).containsExactly("outer");
intercepted.clear();
proxy.inner();
T.line("Q26 proxy.inner() called from outside; advice saw %s", intercepted);
assertThat(intercepted).containsExactly("inner");
}
@Test void q26b_finalMethodsAreNotAdvisedOnCglibProxies() {
List<String> intercepted = new ArrayList<>();
ProxyFactory pf = new ProxyFactory(new Shared.Calculator());
pf.addAdvice((MethodInterceptor) inv -> { intercepted.add(inv.getMethod().getName()); return inv.proceed(); });
Shared.Calculator proxy = (Shared.Calculator) pf.getProxy();
String viaFinal = proxy.finalMethod();
T.line("Q26 final method on a CGLIB proxy: finalMethod() -> \"%s\", advice saw %s", viaFinal, intercepted);
assertThat(intercepted).isEmpty();
}
@Test void q27_applicationEventsAreSynchronousByDefault() {
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) {
ctx.registerBean(Beans.Listener.class);
ctx.refresh();
ctx.publishEvent(new Beans.OrderPlaced("order-1"));
String callerThread = Thread.currentThread().getName();
List<String> received = ctx.getBean(Beans.Listener.class).received;
T.line("Q27 publishEvent(OrderPlaced): listener ran on the caller's thread? %s", received.contains(callerThread));
assertThat(received).containsExactly(callerThread);
}
}
@Test void q28_transactionTemplateIsTemplateAndCallback() {
List<String> calls = new ArrayList<>();
PlatformTransactionManager tm = new PlatformTransactionManager() {
public TransactionStatus getTransaction(TransactionDefinition d) { calls.add("begin"); return new SimpleTransactionStatus(); }
public void commit(TransactionStatus s) { calls.add("commit"); }
public void rollback(TransactionStatus s) { calls.add("rollback"); }
};
TransactionTemplate template = new TransactionTemplate(tm);
String ok = template.execute(status -> { calls.add("work"); return "done"; });
T.line("Q28 TransactionTemplate success path: %s returned %s", calls, ok);
assertThat(calls).containsExactly("begin", "work", "commit");
calls.clear();
assertThatThrownBy(() -> template.execute(status -> { calls.add("work"); throw new IllegalStateException("boom"); }))
.isInstanceOf(IllegalStateException.class);
T.line("Q28 TransactionTemplate when the callback throws: %s", calls);
assertThat(calls).containsExactly("begin", "work", "rollback");
}
@Test void q29_strategyByInjectionOfAMapOfBeans() {
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Beans.CardPayment.class, Beans.UpiPayment.class, Beans.PaymentService.class)) {
Beans.PaymentService svc = ctx.getBean(Beans.PaymentService.class);
T.line("Q29 Map<String, Payment> injected with keys %s", svc.methods());
T.line("Q29 pay(\"upi\", 500) -> %s", svc.pay("upi", 500));
assertThat(svc.methods()).containsExactly("card", "upi");
assertThat(svc.pay("card", 100)).isEqualTo("card:100");
}
}
@Test void q30_prototypeInsideSingletonIsInjectedOnce() {
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Beans.Ticket.class, Beans.StaleHolder.class, Beans.FreshHolder.class)) {
Beans.StaleHolder stale = ctx.getBean(Beans.StaleHolder.class);
Beans.FreshHolder fresh = ctx.getBean(Beans.FreshHolder.class);
boolean staleSame = stale.ticket() == stale.ticket();
boolean freshSame = fresh.ticket() == fresh.ticket();
boolean ctxSame = ctx.getBean(Beans.Ticket.class) == ctx.getBean(Beans.Ticket.class);
T.line("Q30 getBean(Ticket) twice from the context: same instance? %s", ctxSame);
T.line("Q30 prototype injected into a singleton: same Ticket on every call? %s", staleSame);
T.line("Q30 prototype fetched via ObjectProvider: same Ticket on every call? %s", freshSame);
assertThat(ctxSame).isFalse();
assertThat(staleSame).isTrue();
assertThat(freshSame).isFalse();
}
}
}
@@ -0,0 +1,96 @@
package interview;
import static org.assertj.core.api.Assertions.*;
import interview.structural.*;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Proxy;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
/** Questions 19-23. Writes output/04-structural.txt. */
class StructuralTest {
static final Transcript T = new Transcript("04-structural");
@AfterAll static void write() { T.write(); }
@Test void q19_proxyControlsAccessDecoratorAddsBehaviour() {
Storage.RealStorage.instances = 0;
Storage lazy = new Storage.LazyProxy();
int beforeUse = Storage.RealStorage.instances;
lazy.read("a");
lazy.read("b");
T.line("Q19 LazyProxy: RealStorage instances before first read=%d, after two reads=%d", beforeUse, Storage.RealStorage.instances);
assertThat(beforeUse).isZero();
assertThat(Storage.RealStorage.instances).isEqualTo(1);
Storage guarded = new Storage.ProtectionProxy(new Storage.RealStorage());
assertThatThrownBy(() -> guarded.read("secret-1")).isInstanceOf(SecurityException.class).hasMessage("denied: secret-1");
T.line("Q19 ProtectionProxy: read(\"secret-1\") -> SecurityException: denied: secret-1");
List<String> log = new ArrayList<>();
Storage stacked = new Storage.Logging(new Storage.Caching(new Storage.RealStorage()), log); // client composes any order
stacked.read("k");
stacked.read("k");
T.line("Q19 Decorators stacked Logging(Caching(Real)): read(\"k\") twice -> log=%s", log);
assertThat(log).containsExactly("read k", "read k");
}
@Test void q20_jdkDynamicProxyNeedsAnInterface() {
List<String> calls = new ArrayList<>();
Greeter target = new Greeter.Impl();
Greeter proxy = (Greeter) Proxy.newProxyInstance(Greeter.class.getClassLoader(), new Class<?>[] {Greeter.class},
(p, method, args) -> { calls.add(method.getName()); return method.invoke(target, args); });
String out = proxy.greet("Ankur");
T.line("Q20 proxy.greet(\"Ankur\") -> %s; intercepted=%s", out, calls);
T.line("Q20 Proxy.isProxyClass(proxy.getClass())=%s; proxy instanceof Greeter.Impl=%s",
Proxy.isProxyClass(proxy.getClass()), proxy instanceof Greeter.Impl);
assertThat(out).isEqualTo("hello Ankur");
assertThat(Proxy.isProxyClass(proxy.getClass())).isTrue();
assertThat((Object) proxy).isNotInstanceOf(Greeter.Impl.class);
assertThatThrownBy(() -> Proxy.newProxyInstance(Greeter.class.getClassLoader(), new Class<?>[] {Greeter.Impl.class}, (p, m, a) -> null))
.isInstanceOf(IllegalArgumentException.class).hasMessageContaining("is not an interface");
T.line("Q20 newProxyInstance with a class instead of an interface -> IllegalArgumentException (... is not an interface)");
}
@Test void q21_adapterChangesInterfaceFacadeSimplifies() throws Exception {
Adapters.CelsiusSensor sensor = new Adapters.FahrenheitAdapter(new Adapters.LegacyFahrenheitSensor());
T.line("Q21 adapter: 98.6F read through CelsiusSensor -> %.1f C", sensor.celsius());
assertThat(sensor.celsius()).isCloseTo(37.0, within(0.001));
T.line("Q21 facade: watchMovie() -> %s", new Adapters.HomeTheater().watchMovie());
assertThat(new Adapters.HomeTheater().watchMovie()).hasSize(3);
// the JDK's own adapter: bytes -> chars
try (InputStreamReader r = new InputStreamReader(new ByteArrayInputStream("hi".getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8)) {
T.line("Q21 JDK adapter InputStreamReader(InputStream) first char: %s", (char) r.read());
}
}
@Test void q22_compositeTreatsLeafAndContainerAlike() {
FileNode tree = new FileNode.Dir("root", List.of(
new FileNode.File("a.txt", 10),
new FileNode.Dir("sub", List.of(new FileNode.File("b.txt", 20), new FileNode.File("c.txt", 5)))));
T.line("Q22 size() of a leaf=%d, of sub dir=%d, of root=%d",
((FileNode.Dir) tree).children().get(0).size(),
((FileNode.Dir) tree).children().get(1).size(), tree.size());
assertThat(tree.size()).isEqualTo(35);
}
@Test void q23_flyweightSharesIntrinsicState() {
GlyphFactory f = new GlyphFactory();
String text = "hello world";
List<GlyphFactory.Glyph> glyphs = new ArrayList<>();
for (char c : text.toCharArray()) glyphs.add(f.get(c));
T.line("Q23 \"%s\" has %d characters but the pool holds %d Glyph objects", text, text.length(), f.poolSize());
assertThat(glyphs.get(2)).isSameAs(glyphs.get(3)); // both 'l'
assertThat(f.poolSize()).isEqualTo(8);
// the JDK's Integer cache is a flyweight, bounded at 127 by default
Integer a = 127, b = 127, c = 128, d = 128;
T.line("Q23 Integer.valueOf: 127==127 -> %s, 128==128 -> %s", a == b, c == d);
assertThat(a == b).isTrue();
assertThat(c == d).isFalse();
}
}
@@ -0,0 +1,34 @@
package interview;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/** Collects the facts a test asserts and writes them to output/NN-name.txt (module-relative). */
public final class Transcript {
private final String name;
private final List<String> lines = new ArrayList<>();
public Transcript(String name) { this.name = name; }
public void line(String format, Object... args) { lines.add(String.format(format, args)); }
private static int question(String line) {
int end = line.indexOf(' ');
return Integer.parseInt(line.substring(1, end));
}
public void write() {
try {
Path dir = Path.of("output");
Files.createDirectories(dir);
List<String> sorted = new ArrayList<>(lines); // JUnit runs methods in an unspecified order
sorted.sort(java.util.Comparator.comparingInt(Transcript::question));
Files.write(dir.resolve(name + ".txt"), sorted);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}