Java 17中Lambda对象this引用反射赋值失败问题咨询
Lambda反射修改this引用在Java17中的问题解析
问题背景
以下代码在Java 11中可正常执行:
package test; import org.junit.Assert; import org.junit.Test; import java.lang.reflect.Field; import java.util.function.Consumer; public class LambdaArg1ReflectionTest { @Test public void test() throws IllegalAccessException { // 创建捕获外部类this的Lambda Consumer<String> lambda = s -> this.parseInt(s, 7); // 通过反射获取Lambda中的this引用字段 Field lambda_arg1_field = lambda.getClass().getDeclaredFields()[0]; lambda_arg1_field.setAccessible(true); Object lambda_arg1_value = lambda_arg1_field.get(lambda); // 验证字段值为当前类实例 Assert.assertEquals(this, lambda_arg1_value); // 尝试重新赋值相同的实例 lambda_arg1_field.set(lambda, lambda_arg1_value); } private int parseInt(String s, int i) { return Integer.parseInt(s + i); } }
但在Java 17中运行会抛出如下异常:
java.lang.IllegalAccessException: Can not set final test.LambdaArg1ReflectionTest field test.LambdaArg1ReflectionTest$$Lambda$40/0x0000000800c191a8.arg$1 to test.LambdaArg1ReflectionTest at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76) at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:80) at java.base/jdk.internal.reflect.UnsafeQualifiedObjectFieldAccessorImpl.set(UnsafeQualifiedObjectFieldAccessorImpl.java:79) at java.base/java.lang.reflect.Field.set(Field.java:799) at test.LambdaArg1ReflectionTest.test(LambdaArg1ReflectionTest.java:23)
针对上述问题,解答如下:
1. 异常产生的原因是什么?
Java 17对Lambda生成的字节码做了调整:Lambda捕获的外部类this引用对应的字段(示例中的arg$1)被标记为final。同时,Java 17收紧了反射对final字段的修改限制——当final字段已经完成初始化后,通过Field.set()方法修改会直接抛出IllegalAccessException。而在Java 11中,反射允许修改已经初始化的final字段(尽管这本身不符合Java语法规范),所以当时不会报错。
2. Java 17是否不再支持此类操作?
不是完全不支持,而是对final字段的反射修改限制更严格。如果Lambda捕获的字段不是final,依然可以通过反射修改;但对于Lambda捕获的this这类被标记为final的字段,常规反射已经无法修改。
3. 是否存在可行的解决办法?
根据你的实际测试需求(替换Lambda的this为spy对象,捕获方法调用参数),有以下几种可行方案:
方案一:使用可变容器类(推荐,兼容所有Java版本)
创建一个可变的Holder类来持有this引用,让Lambda捕获这个Holder而非直接捕获this,后续只需修改Holder内的引用即可:
// 自定义可变容器类 class InstanceHolder<T> { private T instance; public InstanceHolder(T instance) { this.instance = instance; } public T get() { return instance; } public void set(T instance) { this.instance = instance; } } // 修改测试代码 @Test public void test() { InstanceHolder<LambdaArg1ReflectionTest> holder = new InstanceHolder<>(this); Consumer<String> lambda = s -> holder.get().parseInt(s, 7); // 替换为spy对象 LambdaArg1ReflectionTest spy = Mockito.spy(this); holder.set(spy); // 调用Lambda,此时会使用spy对象的方法 lambda.accept("123"); // 验证方法调用 Mockito.verify(spy).parseInt("123", 7); }
方案二:使用Unsafe类绕过访问限制(不推荐生产环境)
通过JDK内部的Unsafe类直接修改内存中的字段值,它会忽略字段的final修饰符。但Unsafe是内部API,不同JDK版本可能存在兼容性问题,且违反Java的安全规范:
@Test public void test() throws Exception { Consumer<String> lambda = s -> this.parseInt(s, 7); Field lambda_arg1_field = lambda.getClass().getDeclaredFields()[0]; // 获取Unsafe实例 Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); sun.misc.Unsafe unsafe = (sun.misc.Unsafe) unsafeField.get(null); // 计算字段偏移量,直接修改值 long offset = unsafe.objectFieldOffset(lambda_arg1_field); LambdaArg1ReflectionTest spy = Mockito.spy(this); unsafe.putObject(lambda, offset, spy); // 调用Lambda验证 lambda.accept("123"); Mockito.verify(spy).parseInt("123", 7); }
方案三:提前替换实例生成Lambda(测试场景适用)
如果是测试场景,可以先创建spy对象,再基于spy生成Lambda,让Lambda直接捕获spy引用,无需后续修改:
@Test public void test() { LambdaArg1ReflectionTest spy = Mockito.spy(this); // 直接基于spy生成Lambda,捕获的就是spy实例 Consumer<String> lambda = s -> spy.parseInt(s, 7); lambda.accept("123"); Mockito.verify(spy).parseInt("123", 7); }
内容的提问来源于stack exchange,提问作者tsoiland
相关产品推荐
相关产品推荐

