0

JDK version: 1.8.0_291

Target class:

package reflectionStackoverflow.test;

public class ClassA {
    private final static StringBuilder nameStringBuilder = new StringBuilder("1");
    private final static String nameString = "1";
 
    public static void printNameStringBuild() {
        System.out.println("nameStringBuilder: " + nameStringBuilder);
    }
    public static void printNameString() {
        System.out.println("nameString: " + nameString);
    }
}

Test Code:

package reflectionStackoverflow.test;

import org.junit.Test;

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

public class ReflectionTest {
    @Test
    public void test() throws NoSuchFieldException, IllegalAccessException, InstantiationException {
        Class c = ClassA.class;

        Field nameStringBuilder = c.getDeclaredField("nameStringBuilder");
        nameStringBuilder.setAccessible(true);
        Field nameStringBuilderModifiers = nameStringBuilder.getClass().getDeclaredField("modifiers");
        nameStringBuilderModifiers.setAccessible(true);
        nameStringBuilderModifiers.setInt(nameStringBuilder, nameStringBuilder.getModifiers() & ~Modifier.FINAL);
        nameStringBuilder.set(c, new StringBuilder("2"));
        nameStringBuilderModifiers.setInt(nameStringBuilder, nameStringBuilder.getModifiers() & ~Modifier.FINAL);

        Field nameString = c.getDeclaredField("nameString");
        nameString.setAccessible(true);
        Field nameStringModifiers = nameString.getClass().getDeclaredField("modifiers");
        nameStringModifiers.setAccessible(true);
        nameStringModifiers.setInt(nameString, nameString.getModifiers() & ~Modifier.FINAL);
        nameString.set(c, "2");
        nameStringModifiers.setInt(nameString, nameString.getModifiers() & ~Modifier.FINAL);

        ClassA.printNameStringBuild();
        ClassA.printNameString();
    }
}

Test result:

nameStringBuilder: 2
nameString: 1

Conclusion:

StringBuilder field can be modified with reflection, but String field can not be modified.

Question:

Why String can not be modified with reflection?

Is there any way to modify a private final static String value like above StringBuilder case?

wo4wangle
  • 99
  • 6
  • 1
    Why did you tag your question "C++"? – Yksisarvinen Jun 18 '22 at 18:35
  • @Yksisarvinen Sry, I think this question may related to the implementation of Java and mainstream JVM is implemented with cpp, so cpper may know some of it. – wo4wangle Jun 18 '22 at 18:40
  • 1
    `final static String nameString = "1";` is compile-time constant (it is `final` and its value is known at compilation time). This lets compiler use its value directly instead of generating call to *variable* (which speeds things up). So if you use at some point `nameString` compiler will treat it as if you written `"1"` (it is called *inlining*). So in case of `System.out.println("nameString: " + nameString);` ***compiler will generate same bytecode as if you would written `System.out.println("nameString: " + "1");`***. As you see, this bytecode no longer *depends* on `nameString`. – Pshemo Jun 18 '22 at 19:35
  • Changing value of `nameString` will not make `System.out.println("nameString: " + "1");` recompile to use *new value*. What you may want to do is *preventing* nameString from being compile-time constant by assigning to it value which will NOT be [constant expression](https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.28). For instance you could initialize it like `private final static String nameString = new String("1");`. – Pshemo Jun 18 '22 at 19:43
  • 2
    This question may interest you: [Comparing strings with == which are declared final in Java](https://stackoverflow.com/q/19418427) (it is not related to your question, but you can find some more details there about compile-time constant expressions and inlining). Also read last section of [accepted answer](https://stackoverflow.com/a/3301720) in [Change private static final field using Java reflection](https://stackoverflow.com/q/3301635) – Pshemo Jun 18 '22 at 19:48
  • Does this answer your question? [Change private static final field using Java reflection](https://stackoverflow.com/questions/3301635/change-private-static-final-field-using-java-reflection) – Johannes Kuhn Jun 18 '22 at 20:04
  • This should answer your question: [Answer about compile time constants](https://stackoverflow.com/a/3301818/845414) – Johannes Kuhn Jun 18 '22 at 20:05

1 Answers1

2

The Java compiler is required (by the Java Language Specification) to inline constant variables (see https://docs.oracle.com/javase/specs/jls/se17/html/jls-13.html#jls-13.1-110-C for the current version, but the same requirement was already in the JLS for Java 1.6).

You can work around this by making nameString a so called "blank final" (https://docs.oracle.com/javase/specs/jls/se6/html/typesValues.html#10931):

    private static final String nameString;
    static {
        nameString =  "1";
    }

This solves your current problem, but it will get you into trouble if you ever want to upgrade your Java version. Java 12 and later do no longer allow setting the modifier field of the Field class. If your code depends on this hack it will forever be locked to Java versions 11 or lower.

Update to the preceding paragraph: In this case (only running some tests) it might solve your problem but: the JVM is free to optimize the access to the field (even in the case of a blank final field it is declared as final and the JIT optimizer can do optimizations it cannot do with non-final fields)

To show the problem:

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

public class Main {

    public static void main (String[] args) throws Exception {
        System.out.println("Before:");
        System.out.println("Field1: "+Class2.getField1());
        System.out.println("Field2: "+Class2.getField2());
        System.out.println("Field3: "+Class2.getField3());
        System.out.println("Field3: "+Class2.getField3());
        System.out.println();

        Class c = Class2.class;
        Field f1 = c.getDeclaredField("field1");
        Field f2 = c.getDeclaredField("field2");
        Field f3 = c.getDeclaredField("field3");
        changeField(f1);
        changeField(f2);
        changeField(f3);

        System.out.println("After:");
        System.out.println("Field1: "+Class2.getField1());
        System.out.println("Field2: "+Class2.getField2());
        System.out.println("Field3: "+Class2.getField3());
    }

    private static void changeField(Field field) throws Exception {
        field.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        field.set(null, "changed");
    }
}

class Class2 {
    private static final String field1 = "no change"; // constant variable, read access must be inlined
    private static final String field2; // blank final
    private static final String field3; // blank final
    static {
        field2 = "no change";
        field3 = "no change";
    }
    public static String getField1() { return field1; }
    public static String getField2() { return field2; }
    public static String getField3() {
        for (int i = 0; i < 1_000_000; i++) {
            if (!field3.equals("no change")) {
                return field3;
            }
        }
        return field3;
    }
}

It will print out

Before:
Field1: no change
Field2: no change
Field3: no change
Field3: no change

After:
Field1: no change
Field2: changed
Field3: no change

So the JIT changed (at runtime) the code method getField3 to no longer access the field field3 but to load the constant "no change" directly. Since field3 is declared as final this is a perfectly legal optimization.

If you try to run it with Java 17 it will throw an exception:

Exception in thread "main" java.lang.NoSuchFieldException: modifiers
  at java.base/java.lang.Class.getDeclaredField(Class.java:2610)
  at Main.changeField(Main.java:xx)
  at Main.main(Main.java:xx)
Thomas Kläger
  • 17,754
  • 3
  • 23
  • 34
  • @Pshemo are you sure? The JavaDoc for [`Field.setAccessible()`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/reflect/Field.html#setAccessible(boolean)) specifically states: _This method cannot be used to enable write access to a non-modifiable final field. The following fields are non-modifiable: static final fields declared in any class or interface_ (and my tests in Java 17 failed accordingly) – Thomas Kläger Jun 19 '22 at 06:30
  • 1
    non-static `final` fields are modifiable. You could never reliably [change `static final` fields](https://gist.github.com/DasBrain/ca16053f8e073c8a883bee66302c2fee). – Johannes Kuhn Jun 19 '22 at 06:48
  • @ThomasKläger My mistake, in Java 17 we no longer can modify `static final` fields (although IIRC in Java 14 it was still possible - but I may be wrong). – Pshemo Jun 19 '22 at 12:23
  • You could not even modify `static final` fields in Java 8. See my demonstration. Depending on a lot of factors, it may appear to work with Java 8, but that was never supported. – Johannes Kuhn Jun 19 '22 at 13:56
  • @JohannesKuhn I've added an example that shows how a blank `static final` field can be changed in Java 8 and Java 11. Please note that you absolutely must run it with a Java 8 or Java 11 JVM. – Thomas Kläger Jun 19 '22 at 15:33
  • @ThomasKläger Nope: https://www.online-java.com/EW6ASa4ifN I just added an extra call to `getField2` and added a small loop to `getField2`. It says: "no change". – Johannes Kuhn Jun 19 '22 at 15:56
  • @JohannesKuhn thanks for your code. I've included it into the answer to show why changing the field sometimes seems to work and sometimes doesn't work. – Thomas Kläger Jun 19 '22 at 18:54
  • 2
    @JohannesKuhn not even every non-`static` field is modifiable. Final fields of hidden classes (which includes the generated classes for lambda expressions), as well as `record` fields are non-modifiable. And, as always, `System.out`, `System.err`, and `System.in` do not count… – Holger Jun 20 '22 at 09:20
  • @Holger Yeah. Changing `final` fields is tricky, any should be only done on instances that did not escape and are therefore not visible to other threads. And never change trusted final fields. – Johannes Kuhn Jun 20 '22 at 11:38