Final String mit Reflection

Hey, mir ist gerade etwas aufgefallen:

Warum funktioniert das:

import java.lang.reflect.Field;

/**
 *
 * @author Andy
 */
public class MutableString {

    @SuppressWarnings("CallToPrintStackTrace")
    public static void main(String[] args) {
        String s = "Ein Text!";

        System.out.println("Vorher: " + s + "." + s.hashCode());

        try {
            Field field = s.getClass().getDeclaredField("value");
            field.setAccessible(true);
            System.out.println("Get: " + new String((char[])field.get(s)));
            char[] array = "Ein anderer Text!".toCharArray();
            field.set(s, array);
        } catch (IllegalAccessException | IllegalArgumentException | NoSuchFieldException | SecurityException ex) {
            ex.printStackTrace();
        }

        System.out.println("Nachher: " + s + "." + s.hashCode());
    }
}

Aber das nicht:

    import java.lang.reflect.Field;

/**
 *
 * @author Andy
 */
public class MutableString {

    @SuppressWarnings("CallToPrintStackTrace")
    public static void main(String[] args) {
        final String s = "Ein Text!";

        System.out.println("Vorher: " + s + "." + s.hashCode());

        try {
            Field field = s.getClass().getDeclaredField("value");
            field.setAccessible(true);
            System.out.println("Get: " + new String((char[])field.get(s)));
            char[] array = "Ein anderer Text!".toCharArray();
            field.set(s, array);
        } catch (IllegalAccessException | IllegalArgumentException | NoSuchFieldException | SecurityException ex) {
            ex.printStackTrace();
        }

        System.out.println("Nachher: " + s + "." + s.hashCode());
    }
}

Wird bei ner final-Variable der Wert iwo gecached, oder wie entsteht diesen Phänomen?

Es wäre schön gewesen, wenn du gleich dazu geschrieben hättest, wie die Ausgaben aussehen.

Das erste Snippet ergibt bei mir

Vorher: Ein Text!.-2010821378
Get: Ein Text!
Nachher: Ein anderer Text!.-2010821378

Das zweite (Oracle JDK 1.8.0_92)

Vorher: Ein Text!.-2010821378
Get: Ein Text!
Nachher: Ein Text!.-2010821378

Das liegt an den Optimierungen, die der Compiler macht. Wenn man sich den generierten Bytecode anschaut, fällt sofort auf, wieso das so ist:

Die letzte Zeile vom ersten Snippet:

GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "Nachher: "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 1
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
LDC "."
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 1
INVOKEVIRTUAL java/lang/String.hashCode ()I
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V

die selbe Zeile vom zweiten Snippet:

GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "Nachher: Ein Text!."
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
LDC "Ein Text!"
INVOKEVIRTUAL java/lang/String.hashCode ()I
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V

Im zweiten Fall konkateniert der Compiler die Strings also bereits.

Warum der Compiler unterschiedlich optimiert, weiß ich allerdings auch nicht.

1 „Gefällt mir“

Wenn man sich das mit javap -c -v MutableString.class anschaut, werden einige relevante Informationen geliefert. Wie oben schon gesagt ist der Unterschied bei der Ausgabe der hier:

Ohne final :

   117: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
   120: new           #4                  // class java/lang/StringBuilder
   123: dup
   124: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
   127: ldc           #30                 // String Nachher:
   129: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   132: aload_1
   133: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   136: ldc           #8                  // String .
   138: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   141: aload_1
   142: invokevirtual #9                  // Method java/lang/String.hashCode:()I
   145: invokevirtual #10                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   148: invokevirtual #11                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   151: invokevirtual #12                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

Mit final :

   109: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
   112: new           #3                  // class java/lang/StringBuilder
   115: dup
   116: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
   119: ldc           #29                 // String Nachher: Ein Text!.
   121: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   124: ldc           #7                  // String Ein Text!
   126: invokevirtual #8                  // Method java/lang/String.hashCode:()I
   129: invokevirtual #9                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   132: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
   135: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

Und wie auch schon gesagt: Die Zeilen

   119: ldc           #29                 // String Nachher: Ein Text!.
   121: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

zeigen, dass dort schon der konkatenierte String drinsteht. Das -v bei den Parametern von javap bewirkt, dass auch der Constant Pool ausgegeben wird, und der ist in beiden Fällen sehr ähnlich, mit dem Unterschied, dass er ohne das final diese Einträge enthält

 #6 = String             #50           // Vorher:
#30 = String             #78           // Nachher:

und mit dem final diese hier:

 #5 = String             #46           // Vorher: Ein Text!.
#29 = String             #74           // Nachher: Ein Text!.

Ganz vereinfacht, und ohne nun wirklich in den JDK-Sourcecode abzutauchen: Wenn dort „irgendwas“ final ist, dann kann er die Konkatenation schon während des compilierens machen.

Einen Pointer in die JVMS, wo das „verbindlich“ gemacht wird, habe ich auf die Schnelle jetzt nicht gefunden…

(EDIT: Die Information ist sicher da, nur vermutlich etwas versteckt und verteilt - nämlich bestehend aus den Informationen was im Constant Pool steht (nämlich Konstanten :wink: ), und der Frage, was eine „Konstante“ ist…)

1 „Gefällt mir“

zwei Postings mit Compiler-Kauderwelsch,
etwas weltfremd, (fast nur) das jemanden anzubieten, oder ist schon ein früherer Compiler-Gesprächskontext bekannt? :wink:
selbst ich kann da nicht viel herauslesen,

und vor allem ist das sowieso im Detail etwas irrelevant,
was ein einzelner Compiler macht, vielleicht gar nur in einer Einzelsituation ohne genaues Regelwerk, ist vielleicht noch interessant fürs Geschehen, aber ja nun kein Maßstab zur Erklärung des Verhaltens,

ob sich genaues Regelwerk zu sowas finden läßt, wer weiß, grob ist es wohl die Idee, dass der finale String optimiert übertragen wird, alles hat seine Grenzen an Genauigkeit,
fertig an möglichen ersten Einsichten :wink: ,


als naheliegende kleine Variante noch:
bei final String s = "Ein " + Math.random() + " Text!"; wird wiederum nicht optimiert, es braucht die Konstante + final

final String s = "Ein " + Math.PI + " Text!"; wird optimiert…


auch nicht nett als Folge:

    public static void main(String[] args)
        throws Exception
    {
        String s = "Ein Text!";

        Field field = s.getClass().getDeclaredField("value");
        field.setAccessible(true);
        char[] array = "Ein anderer Text!".toCharArray();
        field.set(s, array);


        String s2 = "Ein Text!";
        System.out.println("s2: " + s2 + "." + s2.hashCode());
    }

s2: Ein anderer Text!.522761871

das scheinbar nicht betroffene s2 verhunzt, weil dieselbe eine Konstante im Pool, sowas einfach nicht machen

1 „Gefällt mir“

Wie man die Frage angemessener oder einfacher zusammenfassen könnte, als in meinem abschließenden „Ganz vereinfacht“-Satz, weiß ich nicht.

„Ja, da wird irgendwas gecacht“.

Es wird zwar nichts gecacht, und tatsächlich sind die Prozesse, die da ablaufen, recht kompliziert, und es ist schwierig, mit Sicherheit zu sagen, ob das Verhalten Regeln entspricht, oder nur „zufällig“ so ist, aber … das wären ja Details :wink: Mal im Ernst: Wenn man diese Frage, was im einen oder anderen Fall passiert, einem erfahrenen Java-Programmierer stellen würde, würde der wohl sagen: „Joa, es könnte so oder so sein, ConstantPool, StringPool, Compiler… schwer zu sagen“.

Dass man Strings

nicht

mit Reflection verändern sollte, ist hoffentlich jedem klar…

1 „Gefällt mir“

Natürlich weiß ich, das man String nicht per Reflection verändern sollte :wink: War auch eher zu Testzwecken, ob man so auch final Fields verändern kann. Aber das ist echt interessant mit dem Constant-Pool, das der das einfach zwischenspeichert

Meines Erachtens spielt der String-Pool im Codebeispiel aus dem Eingangspost gar keine Rolle. Erst im von @SlaterB geposteten Beispiel kommt der zum tragen.
Das “erstaunliche” Verhalten aus dem Startposting rührt daher, dass bereits der Compiler die Strings konkateniert.

was heißt hier ‘konkateniert’?

ist der finale String dynamisch zusammengebaut, kann der Compiler ihn nicht direkt von final in die nächste Variable übernehmen,
genauso keine Übernahme wenn erste Variable nicht final

nur im Zusammenspiel beider Bedingungen geht es in der initialen Problem-Variante:
sowohl eine Pool-Konstante + als auch final -> zweite Variable auch diese Pool-Konstante, bereits durch Compiler, und dann Probleme