java 反射 static final_反射修改 static final 变量

本文探讨了Java中使用反射修改`static final`变量的实践,包括测试案例和结果分析。测试发现,基本类型和String类型的`static final`变量无法通过反射修改,而其他类型的变量则可以。在字节码层面,`static final`变量在编译时可能已内联优化,导致反射修改失败。文章还提供了详细的字节码分析以加深理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、测试结论

static final 修饰的基本类型和String类型不能通过反射修改;

二、测试案例

@Test

public void test01() throws Exception {

setFinalStatic(Constant.class.getDeclaredField("i1"), 11);

System.out.println(Constant.i1);

setFinalStatic(Constant.class.getDeclaredField("i2"), 22);

System.out.println(Constant.i2);

setFinalStatic(Constant.class.getDeclaredField("s1"), "change1");

System.out.println(Constant.s1);

setFinalStatic(Constant.class.getDeclaredField("s2"), "change2");

System.out.println(Constant.s2);

System.out.println("----------------");

setFinalStatic(CC.class.getDeclaredField("i1"), 11);

System.out.println(CC.i1);

setFinalStatic(CC.class.getDeclaredField("i2"), 22);

System.out.println(CC.i2);

setFinalStatic(CC.class.getDeclaredField("i3"), 33);

System.out.println(CC.i3);

setFinalStatic(CC.class.getDeclaredField("s1"), "change1");

System.out.println(CC.s1);

setFinalStatic(CC.class.getDeclaredField("s2"), "change2");

System.out.println(CC.s2);

setFinalStatic(CC.class.getDeclaredField("s3"), "change3");

System.out.println(CC.s3);

}

private void setFinalStatic(Field field, Object newValue) throws Exception {

field.setAccessible(true);

Field modifiersField = Field.class.getDeclaredField("modifiers");

modifiersField.setAccessible(true);

modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

field.set(null, newValue);

}

interface Constant {

int i1 = 1;

Integer i2 = 1;

String s1 = "s1";

String s2 = new String("s2");

}

static class CC {

private static final int i1 = 1;

private static final Integer i2 = 1;

private static Integer i3 = 1;

private static final String s1 = "s1";

private static final String s2 = new String("s2");

private static String s3 = "s3";

}

// 打印结果

1

22

s1

change2

----------------

1

22

33

s1

change2

change3

从打印的日志可以看到,正如开篇所说,除了 static final 修饰的基本类型和String类型修改失败,其他的都修改成功了;

但是这里有一个很有意思的现象,在debug的时候显示 i1 已经修改成功了,但是在打印的时候却任然是原来的值;

36c6f72067797aaeab6a050d3be6f603.png

就是因为这个debug然我疑惑了很久,但是仔细分析后感觉这是一个bug,详细原因还暂时未知;

三、案例分析

private void setFinalStatic(Field field, Object newValue) throws Exception {

field.setAccessible(true);

Field modifiersField = Field.class.getDeclaredField("modifiers");

modifiersField.setAccessible(true);

modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

field.set(null, newValue);

}

首先这里修改 static final 值得原理是,将这个 Field 的 FieldAccessor 的 final 给去掉了,否则在 field.set(null, newValue); 的时候, 就会检查 final 而导致失败

// UnsafeIntegerFieldAccessorImpl

if (this.isFinal) {

this.throwFinalFieldIllegalAccessException(var2);

}

而我们在 CC.class.getDeclaredField("i1") 获取的 Field 其实是 clazz 对象中的一个备份,

// Class

private static Field searchFields(Field[] fields, String name) {

String internedName = name.intern();

for (int i = 0; i < fields.length; i++) {

if (fields[i].getName() == internedName) {

return getReflectionFactory().copyField(fields[i]);

}

}

return null;

}

Field copy() {

if (this.root != null)

throw new IllegalArgumentException("Can not copy a non-root Field");

Field res = new Field(clazz, name, type, modifiers, slot, signature, annotations);

res.root = this;

// Might as well eagerly propagate this if already present

res.fieldAccessor = fieldAccessor;

res.overrideFieldAccessor = overrideFieldAccessor;

return res;

}

所以在 field.set(null, newValue); 设置新值得时候,这里就应该是类似值传递和引用传递的问题,复制出来的 field 其实已经修改成功了,但是 root 对象仍然是原来的值,而在打印的时候,其实是直接取的 root 对象的值;

private void setFinalStatic(Field field, Object newValue) throws Exception {

field.setAccessible(true);

// Object o1 = field.get(null);

Field modifiersField = Field.class.getDeclaredField("modifiers");

modifiersField.setAccessible(true);

modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

field.set(null, newValue);

Object o1 = field.get(null);

}

// 打印 11

注意如果这里在去掉 final 之前就取了一次值,就会 set 失败, 因为 Class 默认开启了 useCaches 缓存, get 的时候会获取到 root field 的 FieldAccessor, 后面的重设就会失效;

四、字节码分析

这个问题还可以从字节码的角度分析:

public class CC {

public static final int i1 = 1;

public static final Integer i2 = 1;

public static int i3 = 1;

public final int i4 = 1;

public int i5 = 1;

}

// javap -verbose class

警告: 二进制文件CC包含com.sanzao.CC

Classfile /Users/wangzichao/workspace/test/target/classes/com/sanzao/CC.class

Last modified 2020-7-8; size 572 bytes

MD5 checksum 5f5847cb849315f98177420057130de6

Compiled from "CC.java"

public class com.sanzao.CC

minor version: 0

major version: 52

flags: ACC_PUBLIC, ACC_SUPER

Constant pool:

#1 = Methodref #8.#28 // java/lang/Object."":()V

#2 = Fieldref #7.#29 // com/sanzao/CC.i4:I

#3 = Fieldref #7.#30 // com/sanzao/CC.i5:I

#4 = Methodref #31.#32 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

#5 = Fieldref #7.#33 // com/sanzao/CC.i2:Ljava/lang/Integer;

#6 = Fieldref #7.#34 // com/sanzao/CC.i3:I

#7 = Class #35 // com/sanzao/CC

#8 = Class #36 // java/lang/Object

#9 = Utf8 i1

#10 = Utf8 I

#11 = Utf8 ConstantValue

#12 = Integer 1

#13 = Utf8 i2

#14 = Utf8 Ljava/lang/Integer;

#15 = Utf8 i3

#16 = Utf8 i4

#17 = Utf8 i5

#18 = Utf8

#19 = Utf8 ()V

#20 = Utf8 Code

#21 = Utf8 LineNumberTable

#22 = Utf8 LocalVariableTable

#23 = Utf8 this

#24 = Utf8 Lcom/sanzao/CC;

#25 = Utf8

#26 = Utf8 SourceFile

#27 = Utf8 CC.java

#28 = NameAndType #18:#19 // "":()V

#29 = NameAndType #16:#10 // i4:I

#30 = NameAndType #17:#10 // i5:I

#31 = Class #37 // java/lang/Integer

#32 = NameAndType #38:#39 // valueOf:(I)Ljava/lang/Integer;

#33 = NameAndType #13:#14 // i2:Ljava/lang/Integer;

#34 = NameAndType #15:#10 // i3:I

#35 = Utf8 com/sanzao/CC

#36 = Utf8 java/lang/Object

#37 = Utf8 java/lang/Integer

#38 = Utf8 valueOf

#39 = Utf8 (I)Ljava/lang/Integer;

{

public static final int i1;

descriptor: I

flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

ConstantValue: int 1

public static final java.lang.Integer i2;

descriptor: Ljava/lang/Integer;

flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL

public static int i3;

descriptor: I

flags: ACC_PUBLIC, ACC_STATIC

public final int i4;

descriptor: I

flags: ACC_PUBLIC, ACC_FINAL

ConstantValue: int 1

public int i5;

descriptor: I

flags: ACC_PUBLIC

public com.sanzao.CC();

descriptor: ()V

flags: ACC_PUBLIC

Code:

stack=2, locals=1, args_size=1

0: aload_0

1: invokespecial #1 // Method java/lang/Object."":()V

4: aload_0

5: iconst_1

6: putfield #2 // Field i4:I

9: aload_0

10: iconst_1

11: putfield #3 // Field i5:I

14: return

LineNumberTable:

line 3: 0

line 7: 4

line 8: 9

LocalVariableTable:

Start Length Slot Name Signature

0 15 0 this Lcom/sanzao/CC;

static {};

descriptor: ()V

flags: ACC_STATIC

Code:

stack=1, locals=0, args_size=0

0: iconst_1

1: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

4: putstatic #5 // Field i2:Ljava/lang/Integer;

7: iconst_1

8: putstatic #6 // Field i3:I

11: return

LineNumberTable:

line 5: 0

line 6: 7

}

SourceFile: "CC.java"

#9 = Utf8 i1

#10 = Utf8 I

#11 = Utf8 ConstantValue

#12 = Integer 1

从这里就能看到 i1 其实是在编译的时候就已经初始化了(代码内联)优化, 而 i4, i5 是在构造函数的时候初始化, i2, i3 是在执行 static 阶段初始化, 同时 i2, i3, i4, i5 都会指向一个 Fieldref 对象, 所以在运行阶段就能通过 Fieldref 反射到它真实的值;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值