奇技淫巧:Java中的final字段真的不能修改么?(怎样修改final字段)

  • Post author:
  • Post category:java


原文:

https://zhuanlan.zhihu.com/p/107267834

先说答案:通过反射是可以修改final字段的!

ps:但是修改后的数据能不能生效(正确访问到修改后的数据)就得看情况了,不同的数据类型、不同的数据初始化方式、不同的访问方式都可能导致修改后访问到的数据还是原数据。下边通过几个案例一一验证:

final关键字简介

在Java中,final关键字可以用来修饰类、方法和数据(包括成员变量和局部变量)。

1.final数据

对于一个final数据,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

2.final类

当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。

在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类。

3.final方法

使用final方法的原因有两个:

第一个原因是把方法锁定,以防任何继承类修改它的含义;

第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。——《Java编程思想》第四版第143页

因此,如果只有在想明确禁止 该方法在子类中被覆盖的情况下才将方法设置为final的。

注:类的private方法会隐式地被指定为final方法。

通过Java反射修改final字段

案例1、private final修饰字段(通过构造方法初始化)

A.java

public class A {

    private final String strX;

    private final int intX;

    private final Object objX;

    public A (String strX, int intX, Object objX) {
        this.strX = strX;
        this.intX = intX;
        this.objX = objX;
    }

    public String getStrX() {
        return strX;
    }

    public int getIntX() {
        return intX;
    }

    public Object getObjX() {
        return objX;
    }

}

Main.java

import java.lang.reflect.Field;

public class Main {

    public static void main(String [] args) throws Exception {
        A a = new A("Java牧码人", 1, new StringBuilder("str"));

        System.out.println("修改前strX值 -> " + a.getStrX());
        System.out.println("修改前intX值 -> " + a.getIntX());
        System.out.println("修改前objX值 -> " + a.getObjX().toString());
        System.out.println();

        Field strXField = a.getClass().getDeclaredField("strX");
        strXField.setAccessible(true);
        strXField.set(a, "X,我是Java牧码人!");

        Field intXField = a.getClass().getDeclaredField("intX");
        intXField.setAccessible(true);
        intXField.set(a, 2);

        Field objXField = a.getClass().getDeclaredField("objX");
        objXField.setAccessible(true);
        objXField.set(a, new StringBuilder("objX,我是Java牧码人!"));

        System.out.println("修改后strX值(get方法获取) -> " + a.getStrX());
        System.out.println("修改后intX值(get方法获取) -> " + a.getIntX());
        System.out.println("修改后objX值(get方法获取) -> " + a.getObjX().toString());

        System.out.println("修改后strX值(反射获取) -> " + strXField.get(a));
        System.out.println("修改后intX值(反射获取) -> " + intXField.get(a));
        System.out.println("修改后objX值(反射获取) -> " + objXField.get(a).toString());
    }

}

执行结果:

结论:修改成功

通过构造方法初始化时无论是基本数据类型(byte/char/short/int/long/float/double/boolean)、String类型还是对像类型,通过反射修改字段值后,无论是通过反射访问还是通过方法访问都可以正确的访问到修改后的数据。


案例2、private final修饰字段(直接赋值)

A2.java

public class A2 {

    private final String strX = "strX";

    private final int intX = 1;

    private final Object objX = new StringBuilder("objX");

    public String getStrX() {
        return strX;
    }

    public int getIntX() {
        return intX;
    }

    public Object getObjX() {
        return objX;
    }
}

Main2.java

import java.lang.reflect.Field;

public class Main2 {

    public static void main(String [] args) throws Exception {
        A2 a = new A2();

        System.out.println("修改前strX值 -> " + a.getStrX());
        System.out.println("修改前intX值 -> " + a.getIntX());
        System.out.println("修改前objX值 -> " + a.getObjX().toString());
        System.out.println();

        Field strXField = a.getClass().getDeclaredField("strX");
        strXField.setAccessible(true);
        strXField.set(a, "X,我是Java牧码人!");

        Field intXField = a.getClass().getDeclaredField("intX");
        intXField.setAccessible(true);
        intXField.set(a, 2);

        Field objXField = a.getClass().getDeclaredField("objX");
        objXField.setAccessible(true);
        objXField.set(a, new StringBuilder("objX,我是Java牧码人!"));

        System.out.println("修改后strX值(get方法获取) -> " + a.getStrX());
        System.out.println("修改后intX值(get方法获取) -> " + a.getIntX());
        System.out.println("修改后objX值(get方法获取) -> " + a.getObjX().toString());

        System.out.println("修改后strX值(反射获取) -> " + strXField.get(a));
        System.out.println("修改后intX值(反射获取) -> " + intXField.get(a));
        System.out.println("修改后objX值(反射获取) -> " + objXField.get(a).toString());
    }

}

执行结果:

结论:修改成功

直接赋值时,基本数据类型和String类型修改后通过反射访问可以访问到修改后的值,通过方法访问到仍然是原值(编译优化导致);

对像类型修改后无论是反射访问还是方法访问,访问到的都是新值。


案例3、private final修饰字段(间接赋值)

A3.java

public class A3 {

    private final String strX = genStrX();

    private final int intX = Integer.parseInt("1");

    private final Object objX = genObjX();

    public String getStrX() {
        return strX;
    }

    public int getIntX() {
        return intX;
    }

    public Object getObjX() {
        return objX;
    }

    private String genStrX() {
        return "strX";
    }

    private Object genObjX() {
        return new StringBuilder("objX");
    }

}

Main3.java

import java.lang.reflect.Field;

public class Main3 {

    public static void main(String [] args) throws Exception {
        A3 a = new A3();

        System.out.println("修改前strX值 -> " + a.getStrX());
        System.out.println("修改前intX值 -> " + a.getIntX());
        System.out.println("修改前objX值 -> " + a.getObjX().toString());
        System.out.println();

        Field strXField = a.getClass().getDeclaredField("strX");
        strXField.setAccessible(true);
        strXField.set(a, "X,我是Java牧码人!");

        Field intXField = a.getClass().getDeclaredField("intX");
        intXField.setAccessible(true);
        intXField.set(a, 2);

        Field objXField = a.getClass().getDeclaredField("objX");
        objXField.setAccessible(true);
        objXField.set(a, new StringBuilder("objX,我是Java牧码人!"));

        System.out.println("修改后strX值(get方法获取) -> " + a.getStrX());
        System.out.println("修改后intX值(get方法获取) -> " + a.getIntX());
        System.out.println("修改后objX值(get方法获取) -> " + a.getObjX().toString());

        System.out.println("修改后strX值(反射获取) -> " + strXField.get(a));
        System.out.println("修改后intX值(反射获取) -> " + intXField.get(a));
        System.out.println("修改后objX值(反射获取) -> " + objXField.get(a).toString());
    }

}

执行结果:

结论:修改成功

final字段间接赋值(通过执行其他方法或一定的运算后赋值),无论是基本数据类型(byte/char/short/int/long/float/double/boolean)、String类型还是对像类型,通过反射修改字段值后,无论是通过反射访问还是通过方法访问都可以正确的访问到修改后的新值。

ps:间接赋值是在运行时确定的final值,编译期无法优化替换,故修改后可以立即访问到。

案例4、private static final修饰字段(直接赋值)

A4.java

public class A4 {

    private static final String strX = "strX";

    private static final int intX = 1;

    private static final Object objX = new StringBuilder("objX");

    public String getStrX() {
        return strX;
    }

    public int getIntX() {
        return intX;
    }

    public Object getObjX() {
        return objX;
    }
}

Main4.java

import java.lang.reflect.Field;

public class Main4 {

    public static void main(String [] args) throws Exception {
        A4 a = new A4();

        System.out.println("修改前strX值 -> " + a.getStrX());
        System.out.println("修改前intX值 -> " + a.getIntX());
        System.out.println("修改前objX值 -> " + a.getObjX().toString());
        System.out.println();

        Field strXField = a.getClass().getDeclaredField("strX");
        strXField.setAccessible(true);
        strXField.set(a, "X,我是Java牧码人!");

        Field intXField = a.getClass().getDeclaredField("intX");
        intXField.setAccessible(true);
        intXField.set(a, 2);

        Field objXField = a.getClass().getDeclaredField("objX");
        objXField.setAccessible(true);
        objXField.set(a, new StringBuilder("objX,我是Java牧码人!"));

        System.out.println("修改后strX值(get方法获取) -> " + a.getStrX());
        System.out.println("修改后intX值(get方法获取) -> " + a.getIntX());
        System.out.println("修改后objX值(get方法获取) -> " + a.getObjX().toString());

        System.out.println("修改后strX值(反射获取) -> " + strXField.get(a));
        System.out.println("修改后intX值(反射获取) -> " + intXField.get(a));
        System.out.println("修改后objX值(反射获取) -> " + objXField.get(a).toString());
    }

}

执行结果:

结论:修改报错

字段通过static final修饰时,因为反射无法直接修改同时被static final修饰的变量,因此执行后报错,此时需要先通过反射去掉字段的final修饰符才能修改变量,见如下Main5.java:

Main5.java

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class Main5 {

    public static void main(String [] args) throws Exception {
        A4 a = new A4();

        System.out.println("修改前strX值 -> " + a.getStrX());
        System.out.println("修改前intX值 -> " + a.getIntX());
        System.out.println("修改前objX值 -> " + a.getObjX().toString());
        System.out.println();

        Field strXField = a.getClass().getDeclaredField("strX");
        strXField.setAccessible(true);
        Field modifiers = strXField.getClass().getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.setInt(strXField, strXField.getModifiers() & ~Modifier.FINAL);
        strXField.set(a, "X,我是Java牧码人!");

        Field intXField = a.getClass().getDeclaredField("intX");
        intXField.setAccessible(true);
        modifiers = intXField.getClass().getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.setInt(intXField, intXField.getModifiers() & ~Modifier.FINAL);
        intXField.set(a, 2);

        Field objXField = a.getClass().getDeclaredField("objX");
        objXField.setAccessible(true);
        modifiers = objXField.getClass().getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.setInt(objXField, objXField.getModifiers() & ~Modifier.FINAL);
        objXField.set(a, new StringBuilder("objX,我是Java牧码人!"));

        System.out.println("修改后strX值(get方法获取) -> " + a.getStrX());
        System.out.println("修改后intX值(get方法获取) -> " + a.getIntX());
        System.out.println("修改后objX值(get方法获取) -> " + a.getObjX().toString());
        System.out.println();

        System.out.println("修改后strX值(反射获取) -> " + strXField.get(a));
        System.out.println("修改后intX值(反射获取) -> " + intXField.get(a));
        System.out.println("修改后objX值(反射获取) -> " + objXField.get(a).toString());
    }

}


执行结果:

结论:修改成功

static final修饰的字段,通过反射去掉字段的final修饰符,再通过反射修改变量值可以修改成功,根据数值类型和赋值方式不同,访问结果同案例1、2、3。

案例5、public static final修饰字段(直接赋值)

A6.java

public class A6 {

    public static final String strX = "strX";

    public static final int intX = 1;

    public static final Object objX = new StringBuilder("objX");

}

Main6.java

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

public class Main6 {

    public static void main(String [] args) throws Exception {
        A6 a = new A6();
        System.out.println("修改前strX值 -> " + A6.strX);
        System.out.println("修改前intX值 -> " + A6.intX);
        System.out.println("修改前objX值 -> " + A6.objX.toString());
        System.out.println();

        Field strXField = a.getClass().getDeclaredField("strX");
        strXField.setAccessible(true);
        Field modifiers = strXField.getClass().getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.setInt(strXField, strXField.getModifiers() & ~Modifier.FINAL);
        strXField.set(a, "X,我是Java牧码人!");

        Field intXField = a.getClass().getDeclaredField("intX");
        intXField.setAccessible(true);
        modifiers = intXField.getClass().getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.setInt(intXField, intXField.getModifiers() & ~Modifier.FINAL);
        intXField.set(a, 2);

        Field objXField = a.getClass().getDeclaredField("objX");
        objXField.setAccessible(true);
        modifiers = objXField.getClass().getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.setInt(objXField, objXField.getModifiers() & ~Modifier.FINAL);
        objXField.set(a, new StringBuilder("objX,我是Java牧码人!"));

        System.out.println("修改后strX值 -> " + A6.strX);
        System.out.println("修改后intX值 -> " + A6.intX);
        System.out.println("修改后objX值 -> " + A6.objX.toString());
        System.out.println();

        System.out.println("修改后strX值(反射获取) -> " + strXField.get(a));
        System.out.println("修改后intX值(反射获取) -> " + intXField.get(a));
        System.out.println("修改后objX值(反射获取) -> " + objXField.get(a).toString());
    }

}

执行结果:

结论:修改成功,public static final修饰的字段,修改方式和访问结果同private static final修饰的字段,结论同案例4。


总结

final修饰的字段通过反射可以修改字段的值!

  • 构造方法赋值:

构造方法赋值在JVM编译期不会优化,运行时决定字段的值,修改后通过反射和其他方式访问到的都是新值。

  • 直接赋值:

基本类型、String类型:JVM编译期会优化成常量,导致修改后的值通过反射可以访问到新值,其他方式访问到的仍是旧值。

对象类型:JVM编译期不会优化,运行时决定字段的值,修改后通过反射和其他方式访问到的都是新值。

  • 间接赋值:

间接赋值在JVM编译期不会优化,运行时决定字段的值,修改后通过反射和其他方式访问到的都是新值。

ps:

  1. static final修饰字段时,要想通过反射修改字段需要先通过反射去掉final修饰符才能修改成功;

  2. 如果final字段值是运行时赋值的,则修改后无论通过何种方式访问获得的都是新值;

  3. 基本类型、String类型直接赋值时由于JVM编译优化,编译时期用到字段的地方会直接被字段值替换,导致通过反射修改字段值后用到字段的地方仍是原值,但通过反射访问获取到的是新值(给人的错觉是没修改成功)。



欢迎关注「

Java牧码人



追求技术的路上永无止境



版权声明:本文为adu003原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。