Effectivement final vs final - Comportement différent
Jusqu'à présent, je pensais qu'effectivement final et final sont plus ou moins équivalents et que le JLS les traiterait de manière similaire sinon identique dans le comportement réel. Ensuite, j'ai trouvé ce scénario artificiel:
final int a = 97;
System.out.println(true ? a : 'c'); // outputs a
// versus
int a = 97;
System.out.println(true ? a : 'c'); // outputs 97
Apparemment, le JLS fait une différence importante entre les deux ici et je ne sais pas pourquoi.
Je lis d'autres fils comme
- Différence entre final et effectivement final
- Variable finale vs variable finale
- Que signifie une variable «effectivement définitive»?
mais ils n'entrent pas dans ces détails. Après tout, à un niveau plus large, ils semblent être à peu près équivalents. Mais en creusant plus profondément, ils diffèrent apparemment.
Quelle est la cause de ce comportement, est-ce que quelqu'un peut fournir des définitions JLS qui expliquent cela?
Edit: j'ai trouvé un autre scénario connexe:
final String a = "a";
System.out.println(a + "b" == "ab"); // outputs true
// versus
String a = "a";
System.out.println(a + "b" == "ab"); // outputs false
Donc, la chaîne interning se comporte également différemment ici (je ne veux pas utiliser cet extrait de code dans du vrai code, juste curieux de connaître le comportement différent).
Réponses
Tout d'abord, nous ne parlons que de variables locales . Effectivement final ne s'applique pas aux champs. Ceci est important, car la sémantique des finalchamps est très distincte et est sujette à de lourdes optimisations du compilateur et à des promesses de modèle de mémoire, voir $ 17.5.1 sur la sémantique des champs finaux.
Au niveau de la surface finalet effectively finalpour les variables locales sont en effet identiques. Cependant, le JLS fait une distinction claire entre les deux qui a en fait un large éventail d'effets dans des situations spéciales comme celle-ci.
Prémisse
À partir de JLS§4.12.4 à propos des finalvariables:
Une variable constante est une
finalvariable de type primitif ou de type String initialisée avec une expression constante ( §15.29 ). Le fait qu'une variable soit une variable constante ou non peut avoir des implications en ce qui concerne l'initialisation de classe ( §12.4.1 ), la compatibilité binaire ( §13.1 ), l'accessibilité ( §14.22 ) et l'affectation définie ( §16.1.1 ).
Depuis intest primitive, la variable aest une telle variable constante .
De plus, du même chapitre sur effectively final:
Certaines variables qui ne sont pas déclarées finales sont plutôt considérées comme effectivement finales: ...
Donc, d'après la façon dont cela est libellé, il est clair que dans l'autre exemple, an'est pas considérée comme une variable constante, car elle n'est pas définitive , mais seulement effectivement définitive.
Comportement
Maintenant que nous avons la distinction, voyons ce qui se passe et pourquoi la sortie est différente.
Vous utilisez ? :ici l' opérateur conditionnel , nous devons donc vérifier sa définition. À partir de JLS§15.25 :
Il existe trois types d'expressions conditionnelles, classées selon les deuxième et troisième expressions d'opérande: les expressions conditionnelles booléennes , les expressions conditionnelles numériques et les expressions conditionnelles de référence .
Dans ce cas, nous parlons d' expressions conditionnelles numériques , de JLS§15.25.2 :
Le type d'une expression conditionnelle numérique est déterminé comme suit:
Et c'est la partie où les deux cas sont classés différemment.
effectivement final
La version qui effectively finalcorrespond à cette règle:
Sinon, la promotion numérique générale ( §5.6 ) est appliquée aux deuxième et troisième opérandes, et le type de l'expression conditionnelle est le type promu des deuxième et troisième opérandes.
Quel est le même comportement que si vous le feriez 5 + 'd', c'est int + char-à- dire , ce qui entraîne int. Voir JLS§5.6
La promotion numérique détermine le type promu de toutes les expressions dans un contexte numérique. Le type promu est choisi de telle sorte que chaque expression puisse être convertie en type promu et, dans le cas d'une opération arithmétique, l'opération est définie pour les valeurs du type promu. L'ordre des expressions dans un contexte numérique n'est pas significatif pour la promotion numérique. Les règles sont les suivantes:
[...]
Ensuite, l' élargissement de la conversion primitive ( §5.1.2 ) et le rétrécissement de la conversion primitive ( §5.1.3 ) sont appliqués à certaines expressions, selon les règles suivantes:
Dans un contexte de choix numérique, les règles suivantes s'appliquent:
Si une expression est de type
intet n'est pas une expression constante ( §15.29 ), alors le type promu estint, et les autres expressions qui ne sont pas de typeintsubissent une conversion primitive élargie enint.
Donc , tout est promu intcomme aest un intdéjà. Cela explique la sortie de 97.
final
La version avec la finalvariable correspond à cette règle:
Si l' un des opérandes est de type
ToùTestbyte,shortouchar, et l'autre opérande est une expression constante ( §15.29 ) de typeintdont la valeur est représentable dans le typeT, le type de l'expression conditionnelle estT.
La dernière variable aest de type intet une expression constante (car elle l'est final). Il est représentable comme char, donc le résultat est de type char. Cela conclut la sortie a.
Exemple de chaîne
L'exemple avec l'égalité de chaîne est basé sur la même différence fondamentale, les finalvariables sont traitées comme une expression / variable constante, et effectively finalne l'est pas.
En Java, l' internement de chaînes est basé sur des expressions constantes, d'où
"a" + "b" + "c" == "abc"
est trueaussi bien (n'utilisez pas cette construction dans du code réel).
Voir JLS§3.10.5 :
De plus, un littéral de chaîne fait toujours référence à la même instance de la classe String. En effet, les chaînes littérales - ou, plus généralement , les chaînes qui sont les valeurs des expressions constantes ( §15.29 ) - sont "internées" de manière à partager des instances uniques, en utilisant la méthode
String.intern( §12.5 ).
Facile à ignorer car il s'agit principalement de littéraux, mais cela s'applique également aux expressions constantes.
Un autre aspect est que si la variable est déclarée finale dans le corps de la méthode, elle a un comportement différent d'une variable finale passée en paramètre.
public void testFinalParameters(final String a, final String b) {
System.out.println(a + b == "ab");
}
...
testFinalParameters("a", "b"); // Prints false
tandis que
public void testFinalVariable() {
final String a = "a";
final String b = "b";
System.out.println(a + b == "ab"); // Prints true
}
...
testFinalVariable();
cela se produit parce que le compilateur sait que l'utilisation de final String a = "a"la avariable aura toujours la "a"valeur pour cela aet "a"peut être interchangée sans problème. Différemment, si an'est pas défini finalou s'il est défini finalmais sa valeur est affectée à l'exécution (comme dans l'exemple ci-dessus où final est le aparamètre), le compilateur ne sait rien avant son utilisation. Ainsi, la concaténation se produit au moment de l'exécution et une nouvelle chaîne est générée, sans utiliser le pool interne.
Fondamentalement, le comportement est le suivant: si le compilateur sait qu'une variable est une constante, il peut l'utiliser de la même manière que la constante.
Si la variable n'est pas définie final (ou si elle est définitive mais sa valeur est définie au moment de l'exécution), il n'y a aucune raison pour que le compilateur la traite comme une constante même si sa valeur est égale à une constante et que sa valeur n'est jamais modifiée.