Final String mit Reflection


#1

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?


#2

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.


#3

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
)


#4

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


#5

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



#6

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


#7

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.


#8

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