Effectivement final vs final - Comportement différent

Sep 04 2020

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

66 Zabuzard Sep 04 2020 at 16:44

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 est int, et les autres expressions qui ne sont pas de type intsubissent une conversion primitive élargie en int.

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 TTest byte, shortou char, et l'autre opérande est une expression constante ( §15.29 ) de type intdont la valeur est représentable dans le type T, le type de l'expression conditionnelle est T.

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.

7 DavideLorenzoMARINO Sep 04 2020 at 17:48

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.