reflection: Method/Field/MethodHandle/VarHandle demos, module and final-field rules (JEP 500 on JDK 26), JMH call-cost numbers, contract tests
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b6NtPJHrkan4BXhCAM
This commit is contained in:
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user