diff --git a/README.md b/README.md index 0885be9..e6c86cd 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,16 @@ design-patterns/ └── visitor/ ``` +## Interview questions module + +[`interview/`](interview/) holds 30 design pattern interview questions with runnable code and +tests (Maven, JDK 25, Spring Framework 7.0.10). See +[Top 30 Design Patterns Interview Questions in Java](https://ankurm.com/design-patterns-interview-questions-java/). + ## Prerequisites - Java 25+ (Eclipse Temurin recommended) -- No build tool required — plain `javac` / `java` +- No build tool required for the 23 pattern folders — plain `javac` / `java` (only `interview/` uses Maven) ## Run any pattern diff --git a/interview/.gitignore b/interview/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/interview/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/interview/README.md b/interview/README.md new file mode 100644 index 0000000..86deaf2 --- /dev/null +++ b/interview/README.md @@ -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". diff --git a/interview/output/00-environment.txt b/interview/output/00-environment.txt new file mode 100644 index 0000000..38879bb --- /dev/null +++ b/interview/output/00-environment.txt @@ -0,0 +1,2 @@ +openjdk version "25.0.5" 2026-10-20 LTS +tests: 31 diff --git a/interview/output/01-singleton.txt b/interview/output/01-singleton.txt new file mode 100644 index 0000000..bf1c1e9 --- /dev/null +++ b/interview/output/01-singleton.txt @@ -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 diff --git a/interview/output/02-creational.txt b/interview/output/02-creational.txt new file mode 100644 index 0000000..7b99443 --- /dev/null +++ b/interview/output/02-creational.txt @@ -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!" diff --git a/interview/output/03-behavioral.txt b/interview/output/03-behavioral.txt new file mode 100644 index 0000000..1fb01a5 --- /dev/null +++ b/interview/output/03-behavioral.txt @@ -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 diff --git a/interview/output/04-structural.txt b/interview/output/04-structural.txt new file mode 100644 index 0000000..82f22b9 --- /dev/null +++ b/interview/output/04-structural.txt @@ -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 diff --git a/interview/output/05-spring.txt b/interview/output/05-spring.txt new file mode 100644 index 0000000..1021f32 --- /dev/null +++ b/interview/output/05-spring.txt @@ -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 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 diff --git a/interview/output/06-q18-compile-error.txt b/interview/output/06-q18-compile-error.txt new file mode 100644 index 0000000..45d2021 --- /dev/null +++ b/interview/output/06-q18-compile-error.txt @@ -0,0 +1,4 @@ +Q18Broken.java:9: error: the switch expression does not cover all possible input values + return switch (e) { + ^ +1 error diff --git a/interview/pom.xml b/interview/pom.xml new file mode 100644 index 0000000..c322797 --- /dev/null +++ b/interview/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + com.ankurm.patterns + interview + 1.0.0 + Design patterns interview questions + + + 25 + UTF-8 + 7.0.10 + 6.1.3 + 3.27.7 + + + + + org.springframework + spring-context + ${spring.version} + + + org.springframework + spring-tx + ${spring.version} + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + -Xmx512m + + + + + diff --git a/interview/scripts/run-all.sh b/interview/scripts/run-all.sh new file mode 100755 index 0000000..c9fbd37 --- /dev/null +++ b/interview/scripts/run-all.sh @@ -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 diff --git a/interview/src/broken/Q18Broken.java b/interview/src/broken/Q18Broken.java new file mode 100644 index 0000000..88fee42 --- /dev/null +++ b/interview/src/broken/Q18Broken.java @@ -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 + }; + } +} diff --git a/interview/src/main/java/interview/behavioral/Checkout.java b/interview/src/main/java/interview/behavioral/Checkout.java new file mode 100644 index 0000000..3ea9238 --- /dev/null +++ b/interview/src/main/java/interview/behavioral/Checkout.java @@ -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); } +} diff --git a/interview/src/main/java/interview/behavioral/Editor.java b/interview/src/main/java/interview/behavioral/Editor.java new file mode 100644 index 0000000..1420412 --- /dev/null +++ b/interview/src/main/java/interview/behavioral/Editor.java @@ -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 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(); } +} diff --git a/interview/src/main/java/interview/behavioral/EventSource.java b/interview/src/main/java/interview/behavioral/EventSource.java new file mode 100644 index 0000000..a51937d --- /dev/null +++ b/interview/src/main/java/interview/behavioral/EventSource.java @@ -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> listeners = new ArrayList<>(); + + public void subscribe(Consumer l) { listeners.add(l); } + + public void unsubscribe(Consumer 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 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 l : List.copyOf(listeners)) l.accept(event); + } +} diff --git a/interview/src/main/java/interview/behavioral/Expr.java b/interview/src/main/java/interview/behavioral/Expr.java new file mode 100644 index 0000000..737c13a --- /dev/null +++ b/interview/src/main/java/interview/behavioral/Expr.java @@ -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 accept(Visitor v) { return v.visitNum(this); } + } + record Add(Expr left, Expr right) implements Expr { + public R accept(Visitor v) { return v.visitAdd(this); } + } + record Mul(Expr left, Expr right) implements Expr { + public R accept(Visitor v) { return v.visitMul(this); } + } + + /** Classic GoF visitor: double dispatch through accept(). */ + interface Visitor { + R visitNum(Num n); + R visitAdd(Add a); + R visitMul(Mul m); + } + + R accept(Visitor 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()); + }; + } +} diff --git a/interview/src/main/java/interview/behavioral/Order.java b/interview/src/main/java/interview/behavioral/Order.java new file mode 100644 index 0000000..597f2d4 --- /dev/null +++ b/interview/src/main/java/interview/behavioral/Order.java @@ -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(); } +} diff --git a/interview/src/main/java/interview/behavioral/Pipeline.java b/interview/src/main/java/interview/behavioral/Pipeline.java new file mode 100644 index 0000000..8ab167f --- /dev/null +++ b/interview/src/main/java/interview/behavioral/Pipeline.java @@ -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 handlers = new ArrayList<>(); + public final List 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"; + } +} diff --git a/interview/src/main/java/interview/behavioral/ReportJob.java b/interview/src/main/java/interview/behavioral/ReportJob.java new file mode 100644 index 0000000..197ff36 --- /dev/null +++ b/interview/src/main/java/interview/behavioral/ReportJob.java @@ -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 log = new ArrayList<>(); + + protected abstract String body(); // the step subclasses fill in + + public final List 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 runWith(Supplier body) { + return List.of("open", body.get(), "close"); + } +} diff --git a/interview/src/main/java/interview/creational/Button.java b/interview/src/main/java/interview/creational/Button.java new file mode 100644 index 0000000..9bee40e --- /dev/null +++ b/interview/src/main/java/interview/creational/Button.java @@ -0,0 +1,8 @@ +package interview.creational; + +/** Shared product type for Q8 and Q9. */ +public interface Button { + String theme(); + + String render(); +} diff --git a/interview/src/main/java/interview/creational/Dialog.java b/interview/src/main/java/interview/creational/Dialog.java new file mode 100644 index 0000000..6c4686c --- /dev/null +++ b/interview/src/main/java/interview/creational/Dialog.java @@ -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(); } + } +} diff --git a/interview/src/main/java/interview/creational/Doc.java b/interview/src/main/java/interview/creational/Doc.java new file mode 100644 index 0000000..2fd643e --- /dev/null +++ b/interview/src/main/java/interview/creational/Doc.java @@ -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 tags; + + public Doc(String title, List tags) { + this.title = title; + this.tags = tags; + } + + public String title() { return title; } + public List 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)); } +} diff --git a/interview/src/main/java/interview/creational/GuiFactory.java b/interview/src/main/java/interview/creational/GuiFactory.java new file mode 100644 index 0000000..f5ff6d5 --- /dev/null +++ b/interview/src/main/java/interview/creational/GuiFactory.java @@ -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"; } + }; +} diff --git a/interview/src/main/java/interview/creational/HttpCall.java b/interview/src/main/java/interview/creational/HttpCall.java new file mode 100644 index 0000000..46eae6d --- /dev/null +++ b/interview/src/main/java/interview/creational/HttpCall.java @@ -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 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 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 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); + } + } +} diff --git a/interview/src/main/java/interview/creational/SimpleFactory.java b/interview/src/main/java/interview/creational/SimpleFactory.java new file mode 100644 index 0000000..c42477b --- /dev/null +++ b/interview/src/main/java/interview/creational/SimpleFactory.java @@ -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 ]"; } + } +} diff --git a/interview/src/main/java/interview/singleton/ClassicSingleton.java b/interview/src/main/java/interview/singleton/ClassicSingleton.java new file mode 100644 index 0000000..b19d5b9 --- /dev/null +++ b/interview/src/main/java/interview/singleton/ClassicSingleton.java @@ -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; } +} diff --git a/interview/src/main/java/interview/singleton/DclSingleton.java b/interview/src/main/java/interview/singleton/DclSingleton.java new file mode 100644 index 0000000..af064fa --- /dev/null +++ b/interview/src/main/java/interview/singleton/DclSingleton.java @@ -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); } } +} diff --git a/interview/src/main/java/interview/singleton/DclWithoutVolatile.java b/interview/src/main/java/interview/singleton/DclWithoutVolatile.java new file mode 100644 index 0000000..8210609 --- /dev/null +++ b/interview/src/main/java/interview/singleton/DclWithoutVolatile.java @@ -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; } +} diff --git a/interview/src/main/java/interview/singleton/EagerSingleton.java b/interview/src/main/java/interview/singleton/EagerSingleton.java new file mode 100644 index 0000000..76dc7fd --- /dev/null +++ b/interview/src/main/java/interview/singleton/EagerSingleton.java @@ -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; } +} diff --git a/interview/src/main/java/interview/singleton/EnumSingleton.java b/interview/src/main/java/interview/singleton/EnumSingleton.java new file mode 100644 index 0000000..b913656 --- /dev/null +++ b/interview/src/main/java/interview/singleton/EnumSingleton.java @@ -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; } +} diff --git a/interview/src/main/java/interview/singleton/HardenedSingleton.java b/interview/src/main/java/interview/singleton/HardenedSingleton.java new file mode 100644 index 0000000..3075b47 --- /dev/null +++ b/interview/src/main/java/interview/singleton/HardenedSingleton.java @@ -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; } +} diff --git a/interview/src/main/java/interview/singleton/HolderSingleton.java b/interview/src/main/java/interview/singleton/HolderSingleton.java new file mode 100644 index 0000000..06c9fe3 --- /dev/null +++ b/interview/src/main/java/interview/singleton/HolderSingleton.java @@ -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(); } +} diff --git a/interview/src/main/java/interview/singleton/NaiveLazySingleton.java b/interview/src/main/java/interview/singleton/NaiveLazySingleton.java new file mode 100644 index 0000000..c84869b --- /dev/null +++ b/interview/src/main/java/interview/singleton/NaiveLazySingleton.java @@ -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); } +} diff --git a/interview/src/main/java/interview/singleton/SynchronizedSingleton.java b/interview/src/main/java/interview/singleton/SynchronizedSingleton.java new file mode 100644 index 0000000..4fae660 --- /dev/null +++ b/interview/src/main/java/interview/singleton/SynchronizedSingleton.java @@ -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); } } +} diff --git a/interview/src/main/java/interview/spring/Beans.java b/interview/src/main/java/interview/spring/Beans.java new file mode 100644 index 0000000..f9818bf --- /dev/null +++ b/interview/src/main/java/interview/spring/Beans.java @@ -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 { + 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 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 byName; + + @Autowired + public PaymentService(Map byName) { this.byName = byName; } + + public String pay(String method, int amount) { return byName.get(method).pay(amount); } + + public java.util.Set 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 tickets; + @Autowired public FreshHolder(ObjectProvider tickets) { this.tickets = tickets; } + public Ticket ticket() { return tickets.getObject(); } + } +} diff --git a/interview/src/main/java/interview/spring/ScopeDemo.java b/interview/src/main/java/interview/spring/ScopeDemo.java new file mode 100644 index 0000000..3435b2e --- /dev/null +++ b/interview/src/main/java/interview/spring/ScopeDemo.java @@ -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 { } +} diff --git a/interview/src/main/java/interview/spring/Shared.java b/interview/src/main/java/interview/spring/Shared.java new file mode 100644 index 0000000..37eaa2f --- /dev/null +++ b/interview/src/main/java/interview/spring/Shared.java @@ -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"; } + } +} diff --git a/interview/src/main/java/interview/structural/Adapters.java b/interview/src/main/java/interview/structural/Adapters.java new file mode 100644 index 0000000..448dbdc --- /dev/null +++ b/interview/src/main/java/interview/structural/Adapters.java @@ -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 watchMovie() { + return java.util.List.of(lights.dim(), projector.on(), sound.surround()); + } + } +} diff --git a/interview/src/main/java/interview/structural/FileNode.java b/interview/src/main/java/interview/structural/FileNode.java new file mode 100644 index 0000000..b8de968 --- /dev/null +++ b/interview/src/main/java/interview/structural/FileNode.java @@ -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 children) implements FileNode { + public long size() { return children.stream().mapToLong(FileNode::size).sum(); } + } +} diff --git a/interview/src/main/java/interview/structural/GlyphFactory.java b/interview/src/main/java/interview/structural/GlyphFactory.java new file mode 100644 index 0000000..85974c7 --- /dev/null +++ b/interview/src/main/java/interview/structural/GlyphFactory.java @@ -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 pool = new HashMap<>(); + + public Glyph get(char c) { return pool.computeIfAbsent(c, Glyph::new); } + + public int poolSize() { return pool.size(); } +} diff --git a/interview/src/main/java/interview/structural/Greeter.java b/interview/src/main/java/interview/structural/Greeter.java new file mode 100644 index 0000000..3957508 --- /dev/null +++ b/interview/src/main/java/interview/structural/Greeter.java @@ -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; } + } +} diff --git a/interview/src/main/java/interview/structural/Storage.java b/interview/src/main/java/interview/structural/Storage.java new file mode 100644 index 0000000..f6ec013 --- /dev/null +++ b/interview/src/main/java/interview/structural/Storage.java @@ -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 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 log; + public Logging(Storage inner, List log) { this.inner = inner; this.log = log; } + + public String read(String key) { + log.add("read " + key); + return inner.read(key); + } + } + +} diff --git a/interview/src/test/java/interview/CreationalAndBehavioralTest.java b/interview/src/test/java/interview/CreationalAndBehavioralTest.java new file mode 100644 index 0000000..2183a86 --- /dev/null +++ b/interview/src/test/java/interview/CreationalAndBehavioralTest.java @@ -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 list = new ArrayList<>(List.of(new StringBuilder("a"))); + @SuppressWarnings("unchecked") ArrayList cloned = (ArrayList) 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 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 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.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 twoGot = new ArrayList<>(); + Consumer[] 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[] 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 got = new ArrayList<>(); + Consumer[] 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 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() { + 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); + } +} diff --git a/interview/src/test/java/interview/SingletonInterviewTest.java b/interview/src/test/java/interview/SingletonInterviewTest.java new file mode 100644 index 0000000..1cfc73c --- /dev/null +++ b/interview/src/test/java/interview/SingletonInterviewTest.java @@ -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 race(Supplier getter) throws Exception { + int threads = 8; + CyclicBarrier barrier = new CyclicBarrier(threads); + try (ExecutorService pool = Executors.newFixedThreadPool(threads)) { + List> futures = new ArrayList<>(); + for (int i = 0; i < threads; i++) { + futures.add(pool.submit(() -> { barrier.await(); return getter.get(); })); + } + Set seen = java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>()); + for (Future 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 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 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 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 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 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 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 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(); + } + } +} diff --git a/interview/src/test/java/interview/SpringPatternsTest.java b/interview/src/test/java/interview/SpringPatternsTest.java new file mode 100644 index 0000000..0fb67fa --- /dev/null +++ b/interview/src/test/java/interview/SpringPatternsTest.java @@ -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 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 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 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 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 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(); + } + } +} diff --git a/interview/src/test/java/interview/StructuralTest.java b/interview/src/test/java/interview/StructuralTest.java new file mode 100644 index 0000000..5d6b8ff --- /dev/null +++ b/interview/src/test/java/interview/StructuralTest.java @@ -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 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 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 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(); + } +} diff --git a/interview/src/test/java/interview/Transcript.java b/interview/src/test/java/interview/Transcript.java new file mode 100644 index 0000000..426fa04 --- /dev/null +++ b/interview/src/test/java/interview/Transcript.java @@ -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 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 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); + } + } +}