Effektiv endgültig gegen endgültig - Unterschiedliches Verhalten

104

Bisher dachte ich, dass effektiv final und final mehr oder weniger gleichwertig sind und dass die JLS sie ähnlich behandeln würde, wenn nicht identisch im tatsächlichen Verhalten. Dann fand ich dieses erfundene Szenario:

final int a = 97;
System.out.println(true ? a : 'c'); // outputs a

// versus

int a = 97;
System.out.println(true ? a : 'c'); // outputs 97

Anscheinend macht die JLS hier einen wichtigen Unterschied zwischen den beiden und ich bin mir nicht sicher warum.

Ich lese andere Themen wie

aber sie gehen nicht auf solche Details ein. Auf einer breiteren Ebene scheinen sie schließlich ziemlich gleichwertig zu sein. Aber wenn sie tiefer graben, unterscheiden sie sich anscheinend.

Was verursacht dieses Verhalten? Kann jemand einige JLS-Definitionen bereitstellen, die dies erklären?


Bearbeiten: Ich habe ein anderes verwandtes Szenario gefunden:

final String a = "a";
System.out.println(a + "b" == "ab"); // outputs true

// versus

String a = "a";
System.out.println(a + "b" == "ab"); // outputs false

Daher verhält sich das String-Interning auch hier anders (ich möchte dieses Snippet nicht in echtem Code verwenden, nur neugierig auf das unterschiedliche Verhalten).

Zabuzard
quelle
2
Sehr interessante Frage! Ich würde erwarten, dass sich Java in beiden Fällen gleich verhält, aber jetzt bin ich aufgeklärt. Ich frage mich, ob dies immer das Verhalten war oder ob es sich in früheren Versionen unterscheidet
Lino
8
@Lino Die Formulierung für das letzte Zitat in der großen Antwort ist unter der gleiche ganzen Weg zurück zu Java 6 : „Wenn einer der Operanden vom Typ T , wo T ist byte, shortoder char, und der andere Operand ein konstanter Ausdruck von Typ, intdessen Wert in Typ T darstellbar ist , dann ist der Typ des bedingten Ausdrucks T. " --- Ich habe sogar ein Java 1.0-Dokument in Berkeley gefunden. Gleicher Text . --- Ja, das war schon immer so.
Andreas
1
Die Art und Weise, wie Sie Dinge "finden", ist interessant: P
Gern geschehen

Antworten:

65

Zunächst sprechen wir nur über lokale Variablen . Tatsächlich gilt final nicht für Felder. Dies ist wichtig, da die Semantik für finalFelder sehr unterschiedlich ist und umfangreichen Compiler-Optimierungen und Versprechungen für das Speichermodell unterliegt (siehe 17.5.1 USD zur Semantik der endgültigen Felder).

Auf Oberflächenebene finalund effectively finalfür lokale Variablen sind in der Tat identisch. Das JLS unterscheidet jedoch klar zwischen den beiden, was in solchen besonderen Situationen tatsächlich eine Vielzahl von Auswirkungen hat.


Prämisse

Aus JLS§4.12.4 über finalVariablen:

Eine konstante Variable ist eine finalVariable vom primitiven Typ oder Typ String , die mit einem konstanten Ausdruck initialisiert wird ( §15.29 ). Ob eine Variable eine konstante Variable ist oder nicht, kann Auswirkungen auf die Klasseninitialisierung ( §12.4.1 ), die Binärkompatibilität ( §13.1 ), die Erreichbarkeit ( §14.22 ) und die definitive Zuordnung ( §16.1.1 ) haben.

Da intes primitiv ist, ist die Variable aeine solche konstante Variable .

Weiter aus dem gleichen Kapitel über effectively final:

Bestimmte Variablen, die nicht als endgültig deklariert sind, werden stattdessen als effektiv endgültig betrachtet: ...

So aus dem Weg , dies formuliert wird, ist es klar , dass in dem anderen Beispiel, aist nicht eine konstante Größe betrachtet, wie es ist nicht endgültig , sondern nur effektiv endgültig.


Verhalten

Nachdem wir nun die Unterscheidung getroffen haben, schauen wir nach, was los ist und warum die Ausgabe anders ist.

Sie verwenden hier den bedingten Operator ? :, daher müssen wir seine Definition überprüfen. Aus JLS§15.25 :

Es gibt drei Arten von bedingten Ausdrücken, die nach dem zweiten und dritten Operandenausdruck klassifiziert sind: boolesche bedingte Ausdrücke , numerische bedingte Ausdrücke und bedingte Referenzausdrücke .

In diesem Fall handelt es sich um numerische bedingte Ausdrücke aus JLS§15.25.2 :

Der Typ eines numerischen bedingten Ausdrucks wird wie folgt bestimmt:

Und das ist der Teil, in dem die beiden Fälle unterschiedlich klassifiziert werden.

effektiv endgültig

Die Version, effectively finaldie dieser Regel entspricht:

Andernfalls wird die allgemeine numerische Heraufstufung ( §5.6 ) auf den zweiten und dritten Operanden angewendet, und der Typ des bedingten Ausdrucks ist der heraufgestufte Typ des zweiten und dritten Operanden.

Welches ist das gleiche Verhalten, als ob Sie tun würden 5 + 'd', dh int + charwas zu führt int. Siehe JLS§5.6

Die numerische Heraufstufung bestimmt den heraufgestuften Typ aller Ausdrücke in einem numerischen Kontext. Der heraufgestufte Typ wird so gewählt, dass jeder Ausdruck in den heraufgestuften Typ konvertiert werden kann, und im Fall einer arithmetischen Operation wird die Operation für Werte des heraufgestuften Typs definiert. Die Reihenfolge der Ausdrücke in einem numerischen Kontext ist für die numerische Werbung nicht von Bedeutung. Die Regeln lauten wie folgt:

[...]

Als nächstes werden die Erweiterung der primitiven Konvertierung ( §5.1.2 ) und die Verengung der primitiven Konvertierung ( §5.1.3 ) auf einige Ausdrücke gemäß den folgenden Regeln angewendet:

In einem numerischen Auswahlkontext gelten die folgenden Regeln:

Wenn ein Ausdruck vom Typ ist intund kein konstanter Ausdruck ist ( §15.29 ), ist der heraufgestufte Typ intund andere Ausdrücke, die nicht vom Typ sind, werden inteiner erweiterten primitiven Konvertierung in unterzogen int.

So wird alles so gefördert, intwie aes intschon ist. Das erklärt die Ausgabe von 97.

Finale

Die Version mit der finalVariablen entspricht dieser Regel:

Wenn einer der Operanden - Typ ist , Two Tist byte, shortoder char, und der andere Operand ein konstanter Ausdruck ( §15.29 ) des Typs , intdessen Wert in darstellbare Typ T, dann wird der Typ des Bedingungsausdrucks ist T.

Die letzte Variable aist vom Typ intund ein konstanter Ausdruck (weil es so ist final). Es ist darstellbar als char, daher ist das Ergebnis vom Typ char. Damit ist die Ausgabe abgeschlossen a.


String-Beispiel

Das Beispiel mit der Zeichenfolgengleichheit basiert auf demselben Kernunterschied, finalVariablen werden als konstanter Ausdruck / Variable behandelt und effectively finalnicht.

In Java basiert die String-Internierung daher auf konstanten Ausdrücken

"a" + "b" + "c" == "abc"

ist trueauch (verwenden Sie dieses Konstrukt nicht in echtem Code).

Siehe JLS§3.10.5 :

Darüber hinaus bezieht sich ein String-Literal immer auf dieselbe Instanz der Klasse String. Dies liegt daran, dass Zeichenfolgenliterale - oder allgemeiner Zeichenfolgen, die die Werte konstanter Ausdrücke sind ( §15.29 ) - "interniert" werden, um mithilfe der Methode String.intern( §12.5 ) eindeutige Instanzen gemeinsam zu nutzen .

Leicht zu übersehen, da es sich hauptsächlich um Literale handelt, aber es gilt auch für konstante Ausdrücke.

Zabuzard
quelle
8
Das Problem ist, dass Sie erwarten würden, dass Sie ... ? a : 'c'sich gleich verhalten, unabhängig davon , ob aes sich um eine Variable oder eine Konstante handelt . An dem Ausdruck ist anscheinend nichts auszusetzen. --- Im Gegensatz dazu a + "b" == "ab"ist dies ein schlechter Ausdruck , da Zeichenfolgen mit equals()( Wie vergleiche ich Zeichenfolgen in Java? ) Verglichen werden müssen . Die Tatsache, dass es "versehentlich" funktioniert, wenn aes sich um eine Konstante handelt , ist nur eine Eigenart der Internierung von String-Literalen.
Andreas
5
@Andreas Ja, aber beachten Sie, dass das Internieren von Zeichenfolgen eine klar definierte Funktion von Java ist. Es ist kein Zufall, der sich morgen oder in einer anderen JVM ändern könnte. "a" + "b" + "c" == "abc"muss sich truein einer gültigen Java-Implementierung befinden.
Zabuzard
10
Es ist zwar eine klar definierte Eigenart, aber a + "b" == "ab"immer noch ein falscher Ausdruck . Selbst wenn Sie wissen, dass dies aeine Konstante ist , ist sie zu fehleranfällig, um nicht aufzurufen equals(). Oder vielleicht ist zerbrechlich ein besseres Wort, dh es ist zu wahrscheinlich, dass es auseinander fällt, wenn der Code in Zukunft beibehalten wird.
Andreas
2
Beachten Sie, dass selbst im primären Bereich der effektiv endgültigen Variablen, dh ihrer Verwendung in Lambda-Ausdrücken, der Unterschied das Laufzeitverhalten ändern kann, dh den Unterschied zwischen einem erfassenden und einem nicht erfassenden Lambda-Ausdruck ausmachen kann, wobei letzterer zu einem Singleton ausgewertet wird , aber der erstere produziert ein neues Objekt. Mit anderen Worten, (final) String str = "a"; Stream.of(null, null). <Runnable>map( x -> () -> System.out.println(str)) .reduce((a,b) -> () -> System.out.println(a == b)) .ifPresent(Runnable::run);ändert das Ergebnis, wenn str(nicht) final.
Holger
7

Ein weiterer Aspekt ist, dass die Variable, wenn sie im Hauptteil der Methode als final deklariert wird, ein anderes Verhalten aufweist als eine als Parameter übergebene endgültige Variable.

public void testFinalParameters(final String a, final String b) {
  System.out.println(a + b == "ab");
}

...
testFinalParameters("a", "b"); // Prints false

während

public void testFinalVariable() {
   final String a = "a";
   final String b = "b";
   System.out.println(a + b == "ab");  // Prints true
}

...
testFinalVariable();

es geschieht , weil der Compiler weiß , dass die Verwendung final String a = "a"der aVariable wird immer das hat , "a"so dass Wert aund "a"kann problemlos ausgetauscht werden. Wenn der Compiler anicht definiert finaloder definiert ist final, sein Wert jedoch zur Laufzeit zugewiesen wird (wie im obigen Beispiel, in dem final der aParameter ist), weiß er vor seiner Verwendung nichts. Die Verkettung erfolgt also zur Laufzeit und es wird eine neue Zeichenfolge generiert, die den internen Pool nicht verwendet.


Grundsätzlich lautet das Verhalten: Wenn der Compiler weiß, dass eine Variable eine Konstante ist, kann er sie genauso verwenden wie die Konstante.

Wenn die Variable nicht endgültig definiert ist (oder endgültig ist, ihr Wert jedoch zur Laufzeit definiert ist), gibt es für den Compiler keinen Grund, sie als Konstante zu behandeln, auch wenn ihr Wert einer Konstanten entspricht und ihr Wert niemals geändert wird.

Davide Lorenzo MARINO
quelle
4
Daran ist nichts Seltsames :)
dbl
2
Es ist ein weiterer Aspekt der Frage.
Davide Lorenzo MARINO
5
finelSchlüsselwort, das auf einen Parameter angewendet wird, hat eine andere Semantik als finalauf eine lokale Variable angewendet usw.
dbl
6
Die Verwendung des Parameters ist hier unnötig zu verschleiern. Sie können einfach final String a; a = "a";das gleiche Verhalten tun und bekommen
yawkat