diff --git a/README.md b/README.md
index 0cba8ec..da2a30a 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,7 @@ article; each module's own README has that article's version table, quickstart,
| [`cow`](cow/) | CopyOnWriteArrayList in Java: When It Wins (JMH) and When It Hurts |
| [`sorting`](sorting/) | Java Comparator & Comparable: The Complete Guide |
| [`annotation-processor`](annotation-processor/) | Custom Annotations and Annotation Processors in Java: Generate Code at Compile Time |
+| [`reflection`](reflection/) | Java Reflection and MethodHandles in 2026: How They Work and What They Cost |
## License
diff --git a/pom.xml b/pom.xml
index 7f58b4a..4c506d7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,6 +34,7 @@
cow
sorting
annotation-processor
+ reflection
diff --git a/reflection/README.md b/reflection/README.md
new file mode 100644
index 0000000..2bee305
--- /dev/null
+++ b/reflection/README.md
@@ -0,0 +1,58 @@
+# reflection
+
+Companion code for the ankurm.com post *"Java Reflection and MethodHandles in 2026: How They Work
+and What They Cost."* One module in `java-core-examples`. Everything the post explains lives in
+the post itself (accordions per section); this module is the runnable evidence.
+
+## Versions this was built and tested against
+
+| Component | Version | Notes |
+|---|---|---|
+| JDK | 25.0.5+8 (Temurin, LTS) | All demos and the JMH run. |
+| JDK (comparison only) | 26.0.2.1+1 (Temurin) | Used for the JEP 500 final-field warning in `output/03`. |
+| JMH | 1.37 | 3 warmup + 5 measurement iterations, 2 forks. |
+| JUnit Jupiter | 5.11.0 | Contract tests - they fail if a documented behaviour changes. |
+| Maven | 3.9.11 | |
+| Hardware | 2 vCPU Intel Xeon @ 2.30 GHz VM | Shared sandbox: treat ns figures as relative, not absolute. |
+
+## Quickstart
+
+```bash
+export JAVA_HOME=/path/to/jdk-25
+mvn package # compiles, runs the contract tests, builds the JMH jar
+java src/main/java/com/ankurm/reflection/QuickStart.java
+java -cp target/classes com.ankurm.reflection.ReflectionBasicsDemo
+java -cp target/classes com.ankurm.reflection.ModuleRulesDemo
+java --add-opens java.base/java.lang=ALL-UNNAMED -cp target/classes com.ankurm.reflection.ModuleRulesDemo
+java -cp target/classes com.ankurm.reflection.FinalFieldDemo
+java -cp target/classes com.ankurm.reflection.MethodHandlesDemo
+java -jar target/benchmarks.jar CallBenchmark
+```
+
+`scripts/run-all.sh` regenerates every file in `output/` (set `JDK26_HOME` to include the JDK 26 runs).
+
+## What's in here
+
+| File | What it shows |
+|---|---|
+| `Account.java` | The sample class: public API, private method, private/final fields, a throwing method. |
+| `QuickStart.java` | Self-contained: one call by `Method`, one by `MethodHandle`. Run with `java QuickStart.java`. |
+| `ReflectionBasicsDemo.java` | Inspect, construct, invoke; the five exceptions you will meet; extra stack frames. |
+| `ModuleRulesDemo.java` | `setAccessible` on your class vs. a JDK internal; `--add-opens`. |
+| `FinalFieldDemo.java` | Final instance / static / record fields; constant-inlining trap; JEP 500 on JDK 26. |
+| `MethodHandlesDemo.java` | `invokeExact` vs `invoke`, composition, lookup-time access checks, `VarHandle`. |
+| `CallBenchmark.java` | JMH: direct vs `Method` vs `MethodHandle` vs `Field` vs `VarHandle`, static-final vs instance field. |
+| `ReflectionContractTest.java` | Eight tests pinning the contracts the post states. |
+
+## Captured output
+
+| File | From |
+|---|---|
+| `output/01-basics.txt` | `ReflectionBasicsDemo` |
+| `output/02-module-rules.txt` | `ModuleRulesDemo`, without and with `--add-opens` |
+| `output/03-final-fields.txt` | `FinalFieldDemo` on JDK 25, JDK 26, and JDK 26 with `--illegal-final-field-mutation=deny` |
+| `output/04-method-handles.txt` | `MethodHandlesDemo` |
+| `output/05-call-benchmark.txt` | `CallBenchmark` (non-deterministic; relative ordering is the result) |
+| `output/06-quickstart.txt` | `QuickStart` |
+
+Identity-hash suffixes like `unnamed module @6b95977` change on every run.
diff --git a/reflection/output/01-basics.txt b/reflection/output/01-basics.txt
new file mode 100644
index 0000000..8bec669
--- /dev/null
+++ b/reflection/output/01-basics.txt
@@ -0,0 +1,32 @@
+$ java -cp target/classes com.ankurm.reflection.ReflectionBasicsDemo
+
+== 1. Inspect the class ==
+class: com.ankurm.reflection.Account
+ public long balance[]
+ public void deposit[long]
+ public String owner[]
+ private String secret[]
+ public String[] stack[]
+ public String toString[]
+ public static int twice[int]
+
+== 2. Create and call ==
+created: Account[alice, 50]
+after deposit(25): Account[alice, 75]
+
+== 3. The exceptions you will meet ==
+wrong name -> NoSuchMethodException: com.ankurm.reflection.Account.nope()
+wrong arg type -> IllegalArgumentException: argument type mismatch
+method threw -> InvocationTargetException wrapping java.lang.IllegalArgumentException: over limit: 5000
+private, no opt-in -> IllegalAccessException: class com.ankurm.reflection.ReflectionBasicsDemo cannot access a member of class com.ankurm.reflection.Account with modifiers "private"
+after setAccessible(true): secret-of-alice
+
+== 4. What reflection adds to the stack ==
+direct call:
+ com.ankurm.reflection.Account.stack
+ com.ankurm.reflection.ReflectionBasicsDemo.main
+through Method.invoke:
+ com.ankurm.reflection.Account.stack
+ jdk.internal.reflect.DirectMethodHandleAccessor.invoke
+ java.lang.reflect.Method.invoke
+ com.ankurm.reflection.ReflectionBasicsDemo.main
diff --git a/reflection/output/02-module-rules.txt b/reflection/output/02-module-rules.txt
new file mode 100644
index 0000000..25d14a2
--- /dev/null
+++ b/reflection/output/02-module-rules.txt
@@ -0,0 +1,24 @@
+$ java -cp target/classes com.ankurm.reflection.ModuleRulesDemo
+
+== setAccessible on your own class ==
+Account.owner = carol
+
+== setAccessible on a JDK private field ==
+String lives in module : java.base
+we live in module : unnamed module @6b95977 (unnamed=true)
+java.lang open to us : false
+trySetAccessible : false
+setAccessible threw : InaccessibleObjectException
+ Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @6b95977
+
+$ java --add-opens java.base/java.lang=ALL-UNNAMED -cp target/classes com.ankurm.reflection.ModuleRulesDemo
+
+== setAccessible on your own class ==
+Account.owner = carol
+
+== setAccessible on a JDK private field ==
+String lives in module : java.base
+we live in module : unnamed module @3764951d (unnamed=true)
+java.lang open to us : true
+trySetAccessible : true
+setAccessible worked, String.value is a byte[]
diff --git a/reflection/output/03-final-fields.txt b/reflection/output/03-final-fields.txt
new file mode 100644
index 0000000..a499993
--- /dev/null
+++ b/reflection/output/03-final-fields.txt
@@ -0,0 +1,47 @@
+$ java -cp target/classes com.ankurm.reflection.FinalFieldDemo # JDK 25
+
+java.version = 25.0.5
+
+== 1. final instance field ==
+owner() now: mallory
+
+== 2. static final field ==
+IllegalAccessException: Can not set static final int field com.ankurm.reflection.Account.MAX_DEPOSIT to java.lang.Integer
+
+== 3. record component ==
+IllegalAccessException: Can not set final int field com.ankurm.reflection.FinalFieldDemo$Point.x to java.lang.Integer
+
+== 4. constant inlining: the write 'works' but the getter never notices ==
+Field.get -> 99
+max() -> 5
+
+$ java -cp target/classes com.ankurm.reflection.FinalFieldDemo # JDK 26
+
+java.version = 26.0.2.1
+
+== 1. final instance field ==
+WARNING: Final field owner in class com.ankurm.reflection.Account has been mutated reflectively by class com.ankurm.reflection.FinalFieldDemo in unnamed module @18b4aac2 (file:/tmp/work/java-core-examples/reflection/target/classes/)
+WARNING: Use --enable-final-field-mutation=ALL-UNNAMED to avoid a warning
+WARNING: Mutating final fields will be blocked in a future release unless final field mutation is enabled
+owner() now: mallory
+
+== 2. static final field ==
+IllegalAccessException: Can not set static final int field com.ankurm.reflection.Account.MAX_DEPOSIT to java.lang.Integer
+
+== 3. record component ==
+IllegalAccessException: Can not set final int field com.ankurm.reflection.FinalFieldDemo$Point.x to java.lang.Integer
+
+== 4. constant inlining: the write 'works' but the getter never notices ==
+Field.get -> 99
+max() -> 5
+
+$ java --illegal-final-field-mutation=deny -cp target/classes com.ankurm.reflection.FinalFieldDemo # JDK 26
+
+java.version = 26.0.2.1
+
+== 1. final instance field ==
+Exception in thread "main" java.lang.IllegalAccessException: class com.ankurm.reflection.FinalFieldDemo (in unnamed module @18b4aac2) cannot set final field com.ankurm.reflection.Account.owner (in unnamed module @18b4aac2), unnamed module @18b4aac2 is not allowed to mutate final fields
+ at java.base/java.lang.reflect.Field.preSetFinal(Field.java:1504)
+ at java.base/java.lang.reflect.Field.setFinal(Field.java:1447)
+ at java.base/java.lang.reflect.Field.set(Field.java:909)
+ at com.ankurm.reflection.FinalFieldDemo.main(FinalFieldDemo.java:30)
diff --git a/reflection/output/04-method-handles.txt b/reflection/output/04-method-handles.txt
new file mode 100644
index 0000000..a73ab2a
--- /dev/null
+++ b/reflection/output/04-method-handles.txt
@@ -0,0 +1,31 @@
+$ java -cp target/classes com.ankurm.reflection.MethodHandlesDemo
+
+== 1. Look up once, call many times ==
+twice(21) = 42
+balance() = 50
+handle type = (Account)long
+
+== 2. invokeExact is exact; invoke adapts ==
+invokeExact, wrong return type -> WrongMethodTypeException
+ handle's method type (Account)long but found (Account)Object
+invoke (boxing) = 50 (Long)
+
+== 3. Handles compose ==
+bindTo(acct) = 50
+after deposit10 = Account[erin, 60]
+balance as text = "60"
+
+== 4. Access is checked when you look up, not when you call ==
+findVirtual(private) -> IllegalAccessException
+ no such method: com.ankurm.reflection.Account.secret()String/invokeVirtual
+privateLookupIn -> secret-of-erin
+privateLookupIn(String) -> IllegalAccessException
+ module java.base does not open java.lang to unnamed module @135fbaa4
+
+== 5. VarHandle: typed, atomic field access ==
+get = 60
+CAS(60 -> 100) = true, balance now 100
+CAS(60 -> 200) = false, balance now 100
+getAndAdd(5) = was 100, now 105
+set on final field -> UnsupportedOperationException
+array element CAS-able: counts[1] = 7
diff --git a/reflection/output/05-call-benchmark.txt b/reflection/output/05-call-benchmark.txt
new file mode 100644
index 0000000..82ec4fb
--- /dev/null
+++ b/reflection/output/05-call-benchmark.txt
@@ -0,0 +1,15 @@
+$ java -jar target/benchmarks.jar CallBenchmark
+
+openjdk version "25.0.5" 2026-10-20 LTS
+
+Benchmark Mode Cnt Score Error Units
+CallBenchmark.direct avgt 10 0.516 ± 0.069 ns/op
+CallBenchmark.field_get avgt 10 0.899 ± 0.051 ns/op
+CallBenchmark.methodHandle_instanceField_invokeExact avgt 10 2.600 ± 0.145 ns/op
+CallBenchmark.methodHandle_staticFinal_invokeBoxing avgt 10 2.690 ± 0.262 ns/op
+CallBenchmark.methodHandle_staticFinal_invokeExact avgt 10 0.476 ± 0.038 ns/op
+CallBenchmark.reflection_instanceFieldMethod avgt 10 6.476 ± 1.952 ns/op
+CallBenchmark.reflection_lookupEveryCall avgt 10 21.525 ± 1.756 ns/op
+CallBenchmark.reflection_staticFinalMethod avgt 10 1.208 ± 0.152 ns/op
+CallBenchmark.varHandle_staticFinal_get avgt 10 0.519 ± 0.076 ns/op
+
diff --git a/reflection/output/06-quickstart.txt b/reflection/output/06-quickstart.txt
new file mode 100644
index 0000000..2252fa5
--- /dev/null
+++ b/reflection/output/06-quickstart.txt
@@ -0,0 +1,4 @@
+$ java src/main/java/com/ankurm/reflection/QuickStart.java
+
+reflection : Hello, Ankur
+MethodHandle : Hello, Ankur
diff --git a/reflection/pom.xml b/reflection/pom.xml
new file mode 100644
index 0000000..6902750
--- /dev/null
+++ b/reflection/pom.xml
@@ -0,0 +1,83 @@
+
+
+ 4.0.0
+
+
+ com.ankurm
+ java-core-examples
+ 1.0
+
+
+ reflection
+ reflection
+ Java reflection, MethodHandles and VarHandles: access rules, module encapsulation, final-field mutation, and JMH call-cost numbers.
+
+
+ 1.37
+
+
+
+
+ org.openjdk.jmh
+ jmh-core
+ ${jmh.version}
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.11.0
+ test
+
+
+
+
+ benchmarks
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ 25
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.1
+
+
+ package
+ shade
+
+
+
+ org.openjdk.jmh.Main
+
+
+
+
+
+
+
+
+
diff --git a/reflection/scripts/run-all.sh b/reflection/scripts/run-all.sh
new file mode 100755
index 0000000..ef5c3d8
--- /dev/null
+++ b/reflection/scripts/run-all.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+# Regenerates every file in output/. Needs JDK 25 as JAVA_HOME; JDK26_HOME (optional) adds the
+# JEP 500 comparison run. JMH takes a few minutes.
+set -euo pipefail
+cd "$(dirname "$0")/.."
+
+mvn -q package
+
+# env -u keeps the sandbox's JAVA_TOOL_OPTIONS banner out of committed output.
+J() { env -u JAVA_TOOL_OPTIONS "$JAVA_HOME/bin/java" "$@"; }
+
+echo "--- 01: basics ---"
+{ echo '$ java -cp target/classes com.ankurm.reflection.ReflectionBasicsDemo'; echo
+ J -cp target/classes com.ankurm.reflection.ReflectionBasicsDemo 2>&1; } > output/01-basics.txt
+
+echo "--- 02: module rules ---"
+{ echo '$ java -cp target/classes com.ankurm.reflection.ModuleRulesDemo'; echo
+ J -cp target/classes com.ankurm.reflection.ModuleRulesDemo 2>&1
+ echo; echo '$ java --add-opens java.base/java.lang=ALL-UNNAMED -cp target/classes com.ankurm.reflection.ModuleRulesDemo'; echo
+ J --add-opens java.base/java.lang=ALL-UNNAMED -cp target/classes com.ankurm.reflection.ModuleRulesDemo 2>&1; } > output/02-module-rules.txt
+
+echo "--- 03: final fields (JDK 25, and JDK 26 if JDK26_HOME is set) ---"
+{ echo '$ java -cp target/classes com.ankurm.reflection.FinalFieldDemo # JDK 25'; echo
+ J -cp target/classes com.ankurm.reflection.FinalFieldDemo 2>&1
+ if [ -n "${JDK26_HOME:-}" ]; then
+ echo; echo '$ java -cp target/classes com.ankurm.reflection.FinalFieldDemo # JDK 26'; echo
+ env -u JAVA_TOOL_OPTIONS "$JDK26_HOME/bin/java" -cp target/classes com.ankurm.reflection.FinalFieldDemo 2>&1
+ echo; echo '$ java --illegal-final-field-mutation=deny -cp target/classes com.ankurm.reflection.FinalFieldDemo # JDK 26'; echo
+ env -u JAVA_TOOL_OPTIONS "$JDK26_HOME/bin/java" --illegal-final-field-mutation=deny -cp target/classes com.ankurm.reflection.FinalFieldDemo 2>&1 || true
+ fi; } > output/03-final-fields.txt
+
+echo "--- 04: method handles ---"
+{ echo '$ java -cp target/classes com.ankurm.reflection.MethodHandlesDemo'; echo
+ J -cp target/classes com.ankurm.reflection.MethodHandlesDemo 2>&1; } > output/04-method-handles.txt
+
+echo "--- 05: JMH call cost (a few minutes) ---"
+J -jar target/benchmarks.jar CallBenchmark -rf text -rff /tmp/jmh-reflection.txt > /tmp/jmh-reflection.log 2>&1
+{ echo '$ java -jar target/benchmarks.jar CallBenchmark'; echo
+ J -version 2>&1 | head -1
+ echo; grep -A20 '^Benchmark ' /tmp/jmh-reflection.log | grep -v 'Benchmark result is saved'; } > output/05-call-benchmark.txt
+
+echo "--- 06: single-file quick start ---"
+{ echo '$ java src/main/java/com/ankurm/reflection/QuickStart.java'; echo
+ J src/main/java/com/ankurm/reflection/QuickStart.java 2>&1; } > output/06-quickstart.txt
+echo done
diff --git a/reflection/src/main/java/com/ankurm/reflection/Account.java b/reflection/src/main/java/com/ankurm/reflection/Account.java
new file mode 100644
index 0000000..904c280
--- /dev/null
+++ b/reflection/src/main/java/com/ankurm/reflection/Account.java
@@ -0,0 +1,60 @@
+package com.ankurm.reflection;
+
+/**
+ * The one class every demo in this module pokes at. It has a public API, a private method,
+ * a private mutable field, a private final field, a static constant and a method that throws -
+ * everything reflection has an opinion about.
+ */
+public class Account {
+ public static final int MAX_DEPOSIT = 1_000;
+
+ private final String owner;
+ private long balance;
+
+ public Account(String owner, long balance) {
+ this.owner = owner;
+ this.balance = balance;
+ }
+
+ public Account() {
+ this("nobody", 0);
+ }
+
+ public String owner() {
+ return owner;
+ }
+
+ public long balance() {
+ return balance;
+ }
+
+ public void deposit(long amount) {
+ if (amount > MAX_DEPOSIT) {
+ throw new IllegalArgumentException("over limit: " + amount);
+ }
+ balance += amount;
+ }
+
+ private String secret() {
+ return "secret-of-" + owner;
+ }
+
+ public static int twice(int x) {
+ return x * 2;
+ }
+
+ /** Returns the stack as seen from inside this method - used to show what reflection adds. */
+ public String[] stack() {
+ StackTraceElement[] st = new Throwable().getStackTrace();
+ String[] out = new String[Math.min(st.length, 6)];
+ for (int i = 0; i < out.length; i++) {
+ out[i] = st[i].getClassName() + "." + st[i].getMethodName();
+ }
+ return out;
+ }
+
+ @Override
+ public String toString() {
+ return "Account[" + owner + ", " + balance + "]";
+ }
+}
diff --git a/reflection/src/main/java/com/ankurm/reflection/CallBenchmark.java b/reflection/src/main/java/com/ankurm/reflection/CallBenchmark.java
new file mode 100644
index 0000000..1aa06df
--- /dev/null
+++ b/reflection/src/main/java/com/ankurm/reflection/CallBenchmark.java
@@ -0,0 +1,110 @@
+package com.ankurm.reflection;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.invoke.VarHandle;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.concurrent.TimeUnit;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+/**
+ * What does one call cost? Every variant reads Account.balance(). The two things being tested
+ * are the API and - crucially - whether the JIT can see the handle/Method as a constant
+ * (static final) or has to treat it as an unknown (instance field).
+ */
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 3, time = 1)
+@Measurement(iterations = 5, time = 1)
+@Fork(2)
+public class CallBenchmark {
+
+ static final Method S_METHOD;
+ static final MethodHandle S_HANDLE;
+ static final Field S_FIELD;
+ static final VarHandle S_VARHANDLE;
+
+ static {
+ try {
+ S_METHOD = Account.class.getMethod("balance");
+ S_HANDLE = MethodHandles.lookup().findVirtual(Account.class, "balance",
+ MethodType.methodType(long.class));
+ S_FIELD = Account.class.getDeclaredField("balance");
+ S_FIELD.setAccessible(true);
+ S_VARHANDLE = MethodHandles.privateLookupIn(Account.class, MethodHandles.lookup())
+ .findVarHandle(Account.class, "balance", long.class);
+ } catch (ReflectiveOperationException e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ Account acct = new Account("bench", 42);
+ Method method;
+ MethodHandle handle;
+
+ @Setup
+ public void setup() throws Exception {
+ method = Account.class.getMethod("balance");
+ handle = MethodHandles.lookup().findVirtual(Account.class, "balance",
+ MethodType.methodType(long.class));
+ }
+
+ /** What code generation produces: an ordinary call. */
+ @Benchmark
+ public long direct() {
+ return acct.balance();
+ }
+
+ @Benchmark
+ public Object reflection_staticFinalMethod() throws Exception {
+ return S_METHOD.invoke(acct);
+ }
+
+ @Benchmark
+ public Object reflection_instanceFieldMethod() throws Exception {
+ return method.invoke(acct);
+ }
+
+ /** The classic mistake: look the Method up again on every call. */
+ @Benchmark
+ public Object reflection_lookupEveryCall() throws Exception {
+ return Account.class.getMethod("balance").invoke(acct);
+ }
+
+ @Benchmark
+ public long methodHandle_staticFinal_invokeExact() throws Throwable {
+ return (long) S_HANDLE.invokeExact(acct);
+ }
+
+ @Benchmark
+ public long methodHandle_instanceField_invokeExact() throws Throwable {
+ return (long) handle.invokeExact(acct);
+ }
+
+ @Benchmark
+ public Object methodHandle_staticFinal_invokeBoxing() throws Throwable {
+ return S_HANDLE.invoke(acct);
+ }
+
+ @Benchmark
+ public Object field_get() throws Exception {
+ return S_FIELD.get(acct);
+ }
+
+ @Benchmark
+ public long varHandle_staticFinal_get() {
+ return (long) S_VARHANDLE.get(acct);
+ }
+}
diff --git a/reflection/src/main/java/com/ankurm/reflection/FinalFieldDemo.java b/reflection/src/main/java/com/ankurm/reflection/FinalFieldDemo.java
new file mode 100644
index 0000000..17b8304
--- /dev/null
+++ b/reflection/src/main/java/com/ankurm/reflection/FinalFieldDemo.java
@@ -0,0 +1,63 @@
+package com.ankurm.reflection;
+
+import java.lang.reflect.Field;
+
+/**
+ * Section 4: what happens when reflection tries to change a final field. Run it on JDK 25 and
+ * JDK 26 and compare - JEP 500 (JDK 26) adds a warning for the instance-field case.
+ */
+public class FinalFieldDemo {
+
+ record Point(int x, int y) {}
+
+ /** The initializer is a compile-time constant, so javac copies 5 into every read of max. */
+ static class Limit {
+ private final int max = 5;
+
+ int max() {
+ return max;
+ }
+ }
+
+ public static void main(String[] args) throws Exception {
+ System.out.println("java.version = " + System.getProperty("java.version"));
+
+ System.out.println();
+ System.out.println("== 1. final instance field ==");
+ Account acct = new Account("dave", 0);
+ Field owner = Account.class.getDeclaredField("owner");
+ owner.setAccessible(true);
+ owner.set(acct, "mallory");
+ System.out.println("owner() now: " + acct.owner());
+
+ System.out.println();
+ System.out.println("== 2. static final field ==");
+ Field max = Account.class.getField("MAX_DEPOSIT");
+ max.setAccessible(true);
+ try {
+ max.set(null, 5);
+ } catch (IllegalAccessException e) {
+ System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
+ }
+
+ System.out.println();
+ System.out.println("== 3. record component ==");
+ Point p = new Point(1, 2);
+ Field x = Point.class.getDeclaredField("x");
+ x.setAccessible(true);
+ try {
+ x.set(p, 99);
+ } catch (IllegalAccessException e) {
+ System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
+ }
+
+ System.out.println();
+ System.out.println("== 4. constant inlining: the write 'works' but the getter never notices ==");
+ Limit limit = new Limit();
+ Field f = Limit.class.getDeclaredField("max");
+ f.setAccessible(true);
+ f.set(limit, 99);
+ System.out.println("Field.get -> " + f.get(limit));
+ System.out.println("max() -> " + limit.max());
+ }
+}
diff --git a/reflection/src/main/java/com/ankurm/reflection/MethodHandlesDemo.java b/reflection/src/main/java/com/ankurm/reflection/MethodHandlesDemo.java
new file mode 100644
index 0000000..a82e7f0
--- /dev/null
+++ b/reflection/src/main/java/com/ankurm/reflection/MethodHandlesDemo.java
@@ -0,0 +1,95 @@
+package com.ankurm.reflection;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.invoke.VarHandle;
+import java.lang.invoke.WrongMethodTypeException;
+
+/** Sections 5-6: MethodHandles and VarHandles - same jobs as reflection, checked at lookup time. */
+public class MethodHandlesDemo {
+
+ public static void main(String[] args) throws Throwable {
+ MethodHandles.Lookup lookup = MethodHandles.lookup();
+ Account acct = new Account("erin", 50);
+
+ System.out.println("== 1. Look up once, call many times ==");
+ MethodHandle twice = lookup.findStatic(Account.class, "twice",
+ MethodType.methodType(int.class, int.class));
+ int r = (int) twice.invokeExact(21);
+ System.out.println("twice(21) = " + r);
+
+ MethodHandle balance = lookup.findVirtual(Account.class, "balance",
+ MethodType.methodType(long.class));
+ long b = (long) balance.invokeExact(acct);
+ System.out.println("balance() = " + b);
+ System.out.println("handle type = " + balance.type());
+
+ System.out.println();
+ System.out.println("== 2. invokeExact is exact; invoke adapts ==");
+ try {
+ Object o = (Object) balance.invokeExact(acct);
+ System.out.println(o);
+ } catch (WrongMethodTypeException e) {
+ System.out.println("invokeExact, wrong return type -> " + e.getClass().getSimpleName());
+ System.out.println(" " + e.getMessage());
+ }
+ Object boxed = balance.invoke(acct);
+ System.out.println("invoke (boxing) = " + boxed + " (" + boxed.getClass().getSimpleName() + ")");
+
+ System.out.println();
+ System.out.println("== 3. Handles compose ==");
+ MethodHandle bound = balance.bindTo(acct);
+ System.out.println("bindTo(acct) = " + (long) bound.invokeExact());
+ MethodHandle deposit = lookup.findVirtual(Account.class, "deposit",
+ MethodType.methodType(void.class, long.class));
+ MethodHandle deposit10 = MethodHandles.insertArguments(deposit, 1, 10L);
+ deposit10.invokeExact(acct);
+ System.out.println("after deposit10 = " + acct);
+ MethodHandle asText = MethodHandles.filterReturnValue(balance,
+ lookup.findStatic(String.class, "valueOf", MethodType.methodType(String.class, long.class)));
+ String text = (String) asText.invokeExact(acct);
+ System.out.println("balance as text = \"" + text + "\"");
+
+ System.out.println();
+ System.out.println("== 4. Access is checked when you look up, not when you call ==");
+ try {
+ lookup.findVirtual(Account.class, "secret", MethodType.methodType(String.class));
+ } catch (IllegalAccessException e) {
+ System.out.println("findVirtual(private) -> " + e.getClass().getSimpleName());
+ System.out.println(" " + e.getMessage());
+ }
+ MethodHandles.Lookup priv = MethodHandles.privateLookupIn(Account.class, lookup);
+ MethodHandle secret = priv.findVirtual(Account.class, "secret", MethodType.methodType(String.class));
+ System.out.println("privateLookupIn -> " + (String) secret.invokeExact(acct));
+ try {
+ MethodHandles.privateLookupIn(String.class, lookup);
+ } catch (IllegalAccessException e) {
+ System.out.println("privateLookupIn(String) -> " + e.getClass().getSimpleName());
+ System.out.println(" " + e.getMessage());
+ }
+
+ System.out.println();
+ System.out.println("== 5. VarHandle: typed, atomic field access ==");
+ VarHandle vh = priv.findVarHandle(Account.class, "balance", long.class);
+ System.out.println("get = " + (long) vh.get(acct));
+ boolean won = vh.compareAndSet(acct, 60L, 100L);
+ System.out.println("CAS(60 -> 100) = " + won + ", balance now " + acct.balance());
+ boolean lost = vh.compareAndSet(acct, 60L, 200L);
+ System.out.println("CAS(60 -> 200) = " + lost + ", balance now " + acct.balance());
+ long prev = (long) vh.getAndAdd(acct, 5L);
+ System.out.println("getAndAdd(5) = was " + prev + ", now " + acct.balance());
+
+ VarHandle ownerVh = priv.findVarHandle(Account.class, "owner", String.class);
+ try {
+ ownerVh.set(acct, "mallory");
+ } catch (UnsupportedOperationException e) {
+ System.out.println("set on final field -> " + e.getClass().getSimpleName());
+ }
+
+ VarHandle arr = MethodHandles.arrayElementVarHandle(int[].class);
+ int[] counts = new int[3];
+ arr.getAndAdd(counts, 1, 7);
+ System.out.println("array element CAS-able: counts[1] = " + counts[1]);
+ }
+}
diff --git a/reflection/src/main/java/com/ankurm/reflection/ModuleRulesDemo.java b/reflection/src/main/java/com/ankurm/reflection/ModuleRulesDemo.java
new file mode 100644
index 0000000..ba6a54b
--- /dev/null
+++ b/reflection/src/main/java/com/ankurm/reflection/ModuleRulesDemo.java
@@ -0,0 +1,37 @@
+package com.ankurm.reflection;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InaccessibleObjectException;
+
+/**
+ * Section 3: setAccessible is not a master key. Your own classes are open to you; the JDK's
+ * internals are not unless the command line says so (--add-opens).
+ */
+public class ModuleRulesDemo {
+
+ public static void main(String[] args) throws Exception {
+ System.out.println("== setAccessible on your own class ==");
+ Field owner = Account.class.getDeclaredField("owner");
+ owner.setAccessible(true);
+ System.out.println("Account.owner = " + owner.get(new Account("carol", 1)));
+
+ System.out.println();
+ System.out.println("== setAccessible on a JDK private field ==");
+ Module javaBase = String.class.getModule();
+ Module me = ModuleRulesDemo.class.getModule();
+ System.out.println("String lives in module : " + javaBase.getName());
+ System.out.println("we live in module : " + me + " (unnamed=" + !me.isNamed() + ")");
+ System.out.println("java.lang open to us : " + javaBase.isOpen("java.lang", me));
+
+ Field value = String.class.getDeclaredField("value");
+ System.out.println("trySetAccessible : " + value.trySetAccessible());
+ try {
+ value.setAccessible(true);
+ Object bytes = value.get("abc");
+ System.out.println("setAccessible worked, String.value is a " + bytes.getClass().getSimpleName());
+ } catch (InaccessibleObjectException e) {
+ System.out.println("setAccessible threw : " + e.getClass().getSimpleName());
+ System.out.println(" " + e.getMessage());
+ }
+ }
+}
diff --git a/reflection/src/main/java/com/ankurm/reflection/QuickStart.java b/reflection/src/main/java/com/ankurm/reflection/QuickStart.java
new file mode 100644
index 0000000..99cc793
--- /dev/null
+++ b/reflection/src/main/java/com/ankurm/reflection/QuickStart.java
@@ -0,0 +1,31 @@
+package com.ankurm.reflection;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Method;
+
+/**
+ * The smallest useful tour: one class, called by reflection and by a MethodHandle.
+ * Self-contained - run it with: java QuickStart.java
+ */
+public class QuickStart {
+
+ public static class Greeter {
+ public String greet(String name) {
+ return "Hello, " + name;
+ }
+ }
+
+ public static void main(String[] args) throws Throwable {
+ Greeter g = new Greeter();
+
+ Method m = Greeter.class.getMethod("greet", String.class);
+ System.out.println("reflection : " + m.invoke(g, "Ankur"));
+
+ MethodHandle h = MethodHandles.lookup().findVirtual(Greeter.class, "greet",
+ MethodType.methodType(String.class, String.class));
+ String s = (String) h.invokeExact(g, "Ankur");
+ System.out.println("MethodHandle : " + s);
+ }
+}
diff --git a/reflection/src/main/java/com/ankurm/reflection/ReflectionBasicsDemo.java b/reflection/src/main/java/com/ankurm/reflection/ReflectionBasicsDemo.java
new file mode 100644
index 0000000..9801df3
--- /dev/null
+++ b/reflection/src/main/java/com/ankurm/reflection/ReflectionBasicsDemo.java
@@ -0,0 +1,72 @@
+package com.ankurm.reflection;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Comparator;
+
+/** Section 1-2 of the post: inspect a class, call things, and see the exceptions you will meet. */
+public class ReflectionBasicsDemo {
+
+ public static void main(String[] args) throws Exception {
+ Class> c = Account.class;
+
+ System.out.println("== 1. Inspect the class ==");
+ System.out.println("class: " + c.getName());
+ Method[] methods = c.getDeclaredMethods();
+ Arrays.sort(methods, Comparator.comparing(Method::getName));
+ for (Method m : methods) {
+ System.out.println(" " + Modifier.toString(m.getModifiers()) + " "
+ + m.getReturnType().getSimpleName() + " " + m.getName()
+ + Arrays.toString(m.getParameterTypes()));
+ }
+
+ System.out.println();
+ System.out.println("== 2. Create and call ==");
+ Constructor> ctor = c.getConstructor(String.class, long.class);
+ Object account = ctor.newInstance("alice", 50L);
+ System.out.println("created: " + account);
+
+ Method deposit = c.getMethod("deposit", long.class);
+ deposit.invoke(account, 25L);
+ System.out.println("after deposit(25): " + account);
+
+ System.out.println();
+ System.out.println("== 3. The exceptions you will meet ==");
+ try {
+ c.getMethod("nope");
+ } catch (NoSuchMethodException e) {
+ System.out.println("wrong name -> " + e.getClass().getSimpleName() + ": " + e.getMessage());
+ }
+ try {
+ deposit.invoke(account, "twenty");
+ } catch (IllegalArgumentException e) {
+ System.out.println("wrong arg type -> " + e.getClass().getSimpleName() + ": " + e.getMessage());
+ }
+ try {
+ deposit.invoke(account, 5_000L);
+ } catch (InvocationTargetException e) {
+ System.out.println("method threw -> " + e.getClass().getSimpleName()
+ + " wrapping " + e.getCause());
+ }
+
+ Method secret = c.getDeclaredMethod("secret");
+ try {
+ secret.invoke(account);
+ } catch (IllegalAccessException e) {
+ System.out.println("private, no opt-in -> " + e.getClass().getSimpleName() + ": " + e.getMessage());
+ }
+ secret.setAccessible(true);
+ System.out.println("after setAccessible(true): " + secret.invoke(account));
+
+ System.out.println();
+ System.out.println("== 4. What reflection adds to the stack ==");
+ Account a = new Account("bob", 0);
+ System.out.println("direct call:");
+ for (String f : a.stack()) System.out.println(" " + f);
+ System.out.println("through Method.invoke:");
+ for (String f : (String[]) c.getMethod("stack").invoke(a)) System.out.println(" " + f);
+ }
+}
diff --git a/reflection/src/test/java/com/ankurm/reflection/ReflectionContractTest.java b/reflection/src/test/java/com/ankurm/reflection/ReflectionContractTest.java
new file mode 100644
index 0000000..3cd9bbc
--- /dev/null
+++ b/reflection/src/test/java/com/ankurm/reflection/ReflectionContractTest.java
@@ -0,0 +1,101 @@
+package com.ankurm.reflection;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.invoke.VarHandle;
+import java.lang.invoke.WrongMethodTypeException;
+import java.lang.reflect.Field;
+import java.lang.reflect.InaccessibleObjectException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import org.junit.jupiter.api.Test;
+
+/** Pins the contracts the post claims. If one stops being true on a new JDK, the build fails. */
+class ReflectionContractTest {
+
+ @Test
+ void methodInvokeWrapsTheRealExceptionInInvocationTargetException() throws Exception {
+ Method deposit = Account.class.getMethod("deposit", long.class);
+ InvocationTargetException e = assertThrows(InvocationTargetException.class,
+ () -> deposit.invoke(new Account(), 5_000L));
+ assertEquals(IllegalArgumentException.class, e.getCause().getClass());
+ }
+
+ @Test
+ void privateMethodNeedsSetAccessibleEvenInTheSamePackage() throws Exception {
+ Method secret = Account.class.getDeclaredMethod("secret");
+ assertThrows(IllegalAccessException.class, () -> secret.invoke(new Account("x", 0)));
+ secret.setAccessible(true);
+ assertEquals("secret-of-x", secret.invoke(new Account("x", 0)));
+ }
+
+ @Test
+ void jdkInternalsAreClosedWithoutAddOpens() throws Exception {
+ Field value = String.class.getDeclaredField("value");
+ assertFalse(value.trySetAccessible());
+ assertThrows(InaccessibleObjectException.class, () -> value.setAccessible(true));
+ }
+
+ @Test
+ void staticFinalAndRecordFieldsCannotBeSetEvenAfterSetAccessible() throws Exception {
+ Field max = Account.class.getField("MAX_DEPOSIT");
+ max.setAccessible(true);
+ assertThrows(IllegalAccessException.class, () -> max.set(null, 5));
+
+ Field x = FinalFieldDemo.Point.class.getDeclaredField("x");
+ x.setAccessible(true);
+ assertThrows(IllegalAccessException.class, () -> x.set(new FinalFieldDemo.Point(1, 2), 9));
+ }
+
+ @Test
+ void invokeExactRejectsAMismatchedReturnTypeButInvokeAdapts() throws Throwable {
+ MethodHandle balance = MethodHandles.lookup().findVirtual(Account.class, "balance",
+ MethodType.methodType(long.class));
+ Account a = new Account("a", 7);
+ assertEquals(7L, (long) balance.invokeExact(a));
+ assertThrows(WrongMethodTypeException.class, () -> {
+ Object o = (Object) balance.invokeExact(a);
+ });
+ assertEquals(7L, balance.invoke(a));
+ }
+
+ @Test
+ void methodHandleAccessIsCheckedAtLookupTime() throws Throwable {
+ MethodHandles.Lookup lookup = MethodHandles.lookup();
+ assertThrows(IllegalAccessException.class, () -> lookup.findVirtual(Account.class, "secret",
+ MethodType.methodType(String.class)));
+ MethodHandle secret = MethodHandles.privateLookupIn(Account.class, lookup)
+ .findVirtual(Account.class, "secret", MethodType.methodType(String.class));
+ assertEquals("secret-of-nobody", (String) secret.invokeExact(new Account()));
+ }
+
+ @Test
+ void varHandleOnFinalFieldIsReadOnlyAndCasWorksOnPlainField() throws Throwable {
+ MethodHandles.Lookup priv = MethodHandles.privateLookupIn(Account.class, MethodHandles.lookup());
+ VarHandle owner = priv.findVarHandle(Account.class, "owner", String.class);
+ assertThrows(UnsupportedOperationException.class, () -> owner.set(new Account(), "x"));
+
+ VarHandle bal = priv.findVarHandle(Account.class, "balance", long.class);
+ Account a = new Account("a", 60);
+ assertTrue(bal.compareAndSet(a, 60L, 100L));
+ assertFalse(bal.compareAndSet(a, 60L, 200L));
+ assertEquals(100L, a.balance());
+ }
+
+ @Test
+ void reflectionAddsFramesToTheStack() throws Exception {
+ Account a = new Account();
+ String[] direct = a.stack();
+ String[] viaReflection = (String[]) Account.class.getMethod("stack").invoke(a);
+ // Frame 0 is Account.stack. Frame 1 is the caller: the test directly, an accessor via reflection.
+ assertEquals(ReflectionContractTest.class.getName() + ".reflectionAddsFramesToTheStack", direct[1]);
+ assertEquals("jdk.internal.reflect.DirectMethodHandleAccessor.invoke", viaReflection[1]);
+ assertEquals("java.lang.reflect.Method.invoke", viaReflection[2]);
+ }
+}