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:
@@ -0,0 +1 @@
|
||||
target/
|
||||
@@ -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".
|
||||
@@ -0,0 +1,2 @@
|
||||
openjdk version "25.0.5" 2026-10-20 LTS
|
||||
tests: 31
|
||||
@@ -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
|
||||
@@ -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!"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
Executable
+17
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user