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:
@@ -36,10 +36,16 @@ design-patterns/
|
|||||||
└── visitor/
|
└── 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
|
## Prerequisites
|
||||||
|
|
||||||
- Java 25+ (Eclipse Temurin recommended)
|
- 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
|
## Run any pattern
|
||||||
|
|
||||||
|
|||||||
@@ -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