final关键字修饰的变量一定不能修改吗?
在传统的编程思想中,final 关键字修改的变量一旦被赋值,就无法通过正常的代码进行修改。
相信很多人都是这么想的,包括笔者。
那有没有可能通过不正常的代码进行修改呢?
假设示例类如下。
public class Test {
private final String s = "aaa";
@Override
public String toString() {
return "Test{" + "s='" + s + '\'' + '}';
}
}
通过反射修改尝试修改字符串的值。
public class Main {
public static void main(String[] args) throws Exception {
Class<Test> clazz = Test.class;
Test test = clazz.newInstance();
System.out.println("test = " + test);
Field field = clazz.getDeclaredField("s");
field.setAccessible(true);
field.set(test, "bbb");
System.out.println("test = " + test);
}
}
输入结果如下:
修改前 test = Test{s='aaa'}
修改后 test = Test{s='aaa'}
那如果示例类修改一下呢?
public class Test {
private final String s;
public Test(String s) {
this.s = s;
}
@Override
public String toString() {
return "Test{" + "s='" + s + '\'' + '}';
}
}
再次尝试通过反射修改字符串的值。
public class Main {
public static void main(String[] args) throws Exception {
Class<Test> clazz = Test.class;
Constructor<Test> constructor = clazz.getConstructor(String.class);
Test test = constructor.newInstance("aaa");
System.out.println("test = " + test);
Field field = clazz.getDeclaredField("s");
field.setAccessible(true);
field.set(test, "bbb");
System.out.println("test = " + test);
}
}
输出结果如下。
修改前 test = Test{s='aaa'}
修改后 test = Test{s='bbb'}
观察结果发现,通过构造方法向 final 关键字修饰的变量赋值,可以通过反射方法修改;而直接指定的实例变量无法通过反射方法修改。
final 关键字修改的变量一定需要初始化吗?
在传统的的编程思想中, final 关键字修改的变量必须在程序要么赋予常量(静态变量或者实例变量),要么通过构造方法在实例初始化时指定。
我想没有老铁会不认可这个观点吧。
但,真的如此吗?
接下类做个实验。使用 BCEL(Byte Code Enginerring Library) 开源库(JDK自带)尝试生成一个和前面的测试类相似的类,只是不再构造方法中初始化 final 类型的变量,大概效果如下。
package io.github.lamtong.codegen;
public class Test {
private final String s;
public Test() {
}
}
相信读者已经猜到了,这个类不能通过正常代码实例化,毕竟是动态生成 .class 文件并加载,源文件中不存在这个类。因此同样通过反射来创建实例。动态生成字节码文件和测试代码如下。
package io.github.lamtong;
import com.sun.org.apache.bcel.internal.Const;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.generic.*;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
public class Main {
private static final String CLASS_NAME = "io.github.lamtong.codegen.Test";
public static void main(String[] args) {
CustomClassLoader classLoader = new CustomClassLoader();
byte[] bytes = generateOrdinaryClass();
try {
classLoader.setBytes(bytes);
Class<?> clazz = classLoader.loadClass(CLASS_NAME);
Constructor<?> constructor = clazz.getConstructor();
Object o = constructor.newInstance();
Field field = clazz.getDeclaredField("s");
int modifiers = field.getModifiers();
System.out.println("字段是否 private: " + Modifier.isPrivate(modifiers));
System.out.println("字段是否 final: " + Modifier.isFinal(modifiers));
field.setAccessible(true);
System.out.println("调用无参构成方法生成实例, 修改前属性 s = " + field.get(o));
field.set(o, "aaa");
System.out.println("调用无参构成方法生成实例, 修改后属性 s = " + field.get(o));
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchFieldException |
InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
@SuppressWarnings(value = {"all"})
private static byte[] generateOrdinaryClass() {
ClassGen classGen = new ClassGen(CLASS_NAME, Object.class.getName(), "<generated>", Const.ACC_PUBLIC | Const.ACC_SUPER, new String[]{});
ConstantPoolGen constantPool = classGen.getConstantPool();
classGen.setMinor(0);
classGen.setMajor(52);
// 添加实例变量
FieldGen fieldGen = new FieldGen(Const.ACC_PRIVATE | Const.ACC_FINAL, Type.STRING, "s", constantPool);
classGen.addField(fieldGen.getField());
// 创建无参构造方法
InstructionList list = new InstructionList();
InstructionFactory factory = new InstructionFactory(constantPool);
MethodGen methodGen = new MethodGen(Const.ACC_PUBLIC, Type.VOID, Type.NO_ARGS, new String[]{}, "<init>", CLASS_NAME, list, constantPool);
list.append(new ALOAD(0));
list.append(factory.createInvoke(Object.class.getName(), "<init>", Type.VOID, Type.NO_ARGS, Const.INVOKESPECIAL));
InstructionHandle ret = list.append(InstructionConst.RETURN);
methodGen.setMaxStack();
classGen.addMethod(methodGen.getMethod());
list.dispose();
JavaClass javaClass = classGen.getJavaClass();
try {
javaClass.dump("C:\\Users\\lemon\\Desktop\\newProxy\\Test.class");
} catch (IOException e) {
e.printStackTrace();
}
return javaClass.getBytes();
}
private static final class CustomClassLoader extends ClassLoader {
private byte[] bytes;
public void setBytes(byte[] bytes) {
this.bytes = bytes;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return defineClass(name, bytes, 0, bytes.length, null);
}
}
}
结果如下:
字段是否 private: true
字段是否 final: true
调用无参构成方法生成实例, 修改前属性 s = null
调用无参构成方法生成实例, 修改后属性 s = aaa
意不意外,开不开心?
生成的类允许 final 不被初始化也能正常运行,即不直接指定为常量,也不通过构造方法初始化。
总结如下:
- final 关键字修饰的实例变量或者静态变量若直接指定为常量,则通过反射代码无法完成修改。
- final 关键字修改的实例变量若是通过构造方法初始化的,则通过反射代码可以完成修改。
- final 关键字修改的实例变量不一定需要初始化程序才能运行。对 final 关键字代码进行初始化更多的是编译器的约束和语义的要求。
如有不对,欢迎指正……