我有一个带有私有静态final字段的类,不幸的是,我需要在运行时更改它。

使用反射我得到这个错误:java.lang.IllegalAccessException:不能设置静态最终布尔字段

有什么方法可以改变这个值吗?

Field hack = WarpTransform2D.class.getDeclaredField("USE_HACK");
hack.setAccessible(true);
hack.set(null, true);

当前回答

final字段的全部意义在于一旦设置它就不能重新分配。JVM使用这个保证来维护各个地方的一致性(例如内部类引用外部变量)。所以没有。如果能够这样做,就会破坏JVM!

解决办法不是一开始就宣布它是最终的。

其他回答

在JDK 18中,这将不再可能,因为作为JEP-416 (PR)的一部分,通过invokedynamic和MethodHandles重新实现了核心反射。

下面是Mandy Chung的评论,她是这本不可思议的作品的主要作者。重点是我的。

如果底层字段为final,则field对象具有写访问权限当且仅当 setAccessible(true)已成功用于此Field对象; 电场是非静态的;而且 字段的声明类不是一个隐藏类;而且 字段的声明类不是记录类。

即使是final字段,也可以在静态初始化器之外修改,并且(至少JVM HotSpot)将完美地执行字节码。

问题是Java编译器不允许这样做,但是可以使用objectweb.asm轻松绕过。从JVM规范的角度来看,这是一个无效的类文件,但它通过了字节码验证,然后在JVM HotSpot OpenJDK12下成功加载和初始化:

ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Cl", null, "java/lang/Object", null);
{
    FieldVisitor fv = cw.visitField(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL, "fld", "I", null, null);
    fv.visitEnd();
}
{
    // public void setFinalField1() { //... }
    MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "setFinalField1", "()V", null, null);
    mv.visitMaxs(2, 1);
    mv.visitInsn(Opcodes.ICONST_5);
    mv.visitFieldInsn(Opcodes.PUTSTATIC, "Cl", "fld", "I");
    mv.visitInsn(Opcodes.RETURN);
    mv.visitEnd();
}
{
    // public void setFinalField2() { //... }
    MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "setFinalField2", "()V", null, null);
    mv.visitMaxs(2, 1);
    mv.visitInsn(Opcodes.ICONST_2);
    mv.visitFieldInsn(Opcodes.PUTSTATIC, "Cl", "fld", "I");
    mv.visitInsn(Opcodes.RETURN);
    mv.visitEnd();
}
cw.visitEnd();

在Java中,该类大致如下所示:

public class Cl{
    private static final int fld;

    public static void setFinalField1(){
        fld = 5;
    }

    public static void setFinalField2(){
        fld = 2;
    }
}

它不能用javac编译,但可以由JVM加载和执行。

JVM HotSpot对这样的类有特殊处理,因为它阻止这样的“常量”参与常量折叠。这个检查是在类初始化的字节码重写阶段完成的:

// Check if any final field of the class given as parameter is modified
// outside of initializer methods of the class. Fields that are modified
// are marked with a flag. For marked fields, the compilers do not perform
// constant folding (as the field can be changed after initialization).
//
// The check is performed after verification and only if verification has
// succeeded. Therefore, the class is guaranteed to be well-formed.
InstanceKlass* klass = method->method_holder();
u2 bc_index = Bytes::get_Java_u2(bcp + prefix_length + 1);
constantPoolHandle cp(method->constants());
Symbol* ref_class_name = cp->klass_name_at(cp->klass_ref_index_at(bc_index));
if (klass->name() == ref_class_name) {
   Symbol* field_name = cp->name_ref_at(bc_index);
   Symbol* field_sig = cp->signature_ref_at(bc_index);

   fieldDescriptor fd;
   if (klass->find_field(field_name, field_sig, &fd) != NULL) {
      if (fd.access_flags().is_final()) {
         if (fd.access_flags().is_static()) {
            if (!method->is_static_initializer()) {
               fd.set_has_initialized_final_update(true);
            }
          } else {
            if (!method->is_object_initializer()) {
              fd.set_has_initialized_final_update(true);
            }
          }
        }
      }
    }
}

JVM HotSpot检查的唯一限制是final字段不应该在final字段声明所在的类之外修改。

如果你的字段是私有的,你可以这样做:

MyClass myClass= new MyClass();
Field aField= myClass.getClass().getDeclaredField("someField");
aField.setAccessible(true);
aField.set(myClass, "newValueForAString");

和抛出/处理NoSuchFieldException

这里的许多答案都很有用,但我发现没有一个在Android上特别适用。我甚至是joor的Reflect的忠实用户,无论是它还是apache的FieldUtils——在这里的一些答案中都提到了这一点。

Android的问题

这样做的根本原因是,在Android上,field类中没有修饰符字段,这使得任何涉及这段代码的建议(如在标记的答案中)都是无用的:

Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

事实上,引用FieldUtils.removeFinalModifier():

// Do all JREs implement Field with a private ivar called "modifiers"?
final Field modifiersField = Field.class.getDeclaredField("modifiers");

所以,答案是否定的……

解决方案

非常简单——字段名是accessFlags,而不是修饰符。这招很管用:

Field accessFlagsField = Field.class.getDeclaredField("accessFlags");
accessFlagsField.setAccessible(true);
accessFlagsField.setInt(field, field.getModifiers() & ~Modifier.FINAL);

旁注#1:无论字段在类中是否是静态的,这都可以工作。

旁注#2:鉴于字段本身可能是私有的,建议也启用对字段本身的访问,使用field. setaccessible (true)(除了accessFlagsField.setAccessible(true))。

在部署到JDK 1.8u91之前,接受的答案对我来说是有效的。 然后我意识到它在野外失败了。集(null, newValue);行,当我在调用setFinalStatic方法之前通过反射读取值。

可能读取导致了Java反射内部的某种不同设置(即失败情况下的sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImpl,而不是成功情况下的sun.reflect.UnsafeStaticObjectFieldAccessorImpl),但我没有进一步详细说明。

因为我需要在旧值的基础上临时设置新值,然后再将旧值设置回来,所以我对signature做了一点改变,在外部提供计算功能的同时也返回旧值:

public static <T> T assignFinalField(Object object, Class<?> clazz, String fieldName, UnaryOperator<T> newValueFunction) {
    Field f = null, ff = null;
    try {
        f = clazz.getDeclaredField(fieldName);
        final int oldM = f.getModifiers();
        final int newM = oldM & ~Modifier.FINAL;
        ff = Field.class.getDeclaredField("modifiers");
        ff.setAccessible(true);
        ff.setInt(f,newM);
        f.setAccessible(true);

        T result = (T)f.get(object);
        T newValue = newValueFunction.apply(result);

        f.set(object,newValue);
        ff.setInt(f,oldM);

        return result;
    } ...

然而,对于一般情况,这是不够的。