final关键字的一些思考

本文探讨了Java中final关键字修饰的变量是否能被修改,指出直接赋值的final变量不能通过反射修改,但通过构造方法初始化的final变量可以。还提到即使final变量未初始化,动态生成的类仍可运行。

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

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 不被初始化也能正常运行,即不直接指定为常量,也不通过构造方法初始化。

总结如下:

  1. final 关键字修饰的实例变量或者静态变量若直接指定为常量,则通过反射代码无法完成修改。
  2. final 关键字修改的实例变量若是通过构造方法初始化的,则通过反射代码可以完成修改。
  3. final 关键字修改的实例变量不一定需要初始化程序才能运行。对 final 关键字代码进行初始化更多的是编译器的约束和语义的要求。

如有不对,欢迎指正……

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值