La finale est mal définie?

Tout d’abord, un casse-tête: qu’est-ce que le code suivant imprime?

public class RecursiveStatic { public static void main(Ssortingng[] args) { System.out.println(scale(5)); } private static final long X = scale(10); private static long scale(long value) { return X * value; } } 

Répondre:

0

Spoilers ci-dessous.


Si vous imprimez X en échelle (long) et redéfinissez X = scale(10) + 3 , les impressions seront X = 0 puis X = 3 . Cela signifie que X est temporairement défini sur 0 et plus tard défini sur 3 . C’est une violation de la final !

Le modificateur statique, associé au modificateur final, est également utilisé pour définir des constantes. Le dernier modificateur indique que la valeur de ce champ ne peut pas changer .

Source: https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [soulignement ajouté]


Ma question: Est-ce un bug? La final est mal définie?


Voici le code qui m’intéresse. X prendra deux valeurs différentes: 0 et 3 . Je crois que cela constitue une violation de la final .

 public class RecursiveStatic { public static void main(Ssortingng[] args) { System.out.println(scale(5)); } private static final long X = scale(10) + 3; private static long scale(long value) { System.out.println("X = " + X); return X * value; } } 

Cette question a été signalée comme un possible doublon de l’ordre d’initialisation du champ final statique Java . Je pense que cette question n’est pas un doublon puisque l’autre question concerne l’ordre d’initialisation alors que ma question concerne une initialisation cyclique associée au tag final . De l’autre seule question, je ne pourrais pas comprendre pourquoi le code dans ma question ne fait pas d’erreur.

Ceci est particulièrement clair en regardant le résultat obtenu par ernesto: quand a est marqué avec final , il obtient la sortie suivante:

 a=5 a=5 

ce qui n’implique pas l’essentiel de ma question: comment une variable final change-t-elle sa variable?

Une trouvaille très intéressante. Pour le comprendre, nous devons explorer la spécification du langage Java ( JLS ).

La raison en est que la final ne permet qu’une seule affectation . La valeur par défaut, cependant, n’est pas une affectation . En fait, chaque variable ( variable de classe, variable d’instance, composant de tableau) pointe vers sa valeur par défaut depuis le début, avant les affectations . La première affectation modifie alors la référence.


Variables de classe et valeur par défaut

Jetez un oeil à l’exemple suivant:

 private static Object x; public static void main(Ssortingng[] args) { System.out.println(x); // Prints 'null' } 

Nous n’avons pas explicitement assigné une valeur à x , bien qu’elle pointe sur null , sa valeur par défaut. Comparez cela au §4.12.5 :

Valeurs initiales des variables

Chaque variable de classe, variable d’ instance ou composant de tableau est initialisée avec une valeur par défaut lors de sa création ( §15.9 , §15.10.2 )

Notez que cela ne vaut que pour ce type de variables, comme dans notre exemple. Il ne tient pas pour les variables locales, voir l’exemple suivant:

 public static void main(Ssortingng[] args) { Object x; System.out.println(x); // Comstack-time error: // variable x might not have been initialized } 

Du même paragraphe JLS:

Une variable locale ( §14.4 , §14.14 ) doit recevoir explicitement une valeur avant d’être utilisée, soit par initialisation ( §14.4 ), soit par affectation ( §15.26 ), de manière à pouvoir être vérifiée à l’aide des règles d’affectation définie ( § 16 (Affectation définitive) ).


Variables finales

Nous examinons maintenant la version final du §4.12.4 :

Variables finales

Une variable peut être déclarée finale . Une variable finale ne peut être affectée qu’une seule fois . Il s’agit d’une erreur de compilation si une variable finale est affectée à moins qu’elle soit définitivement non affectée immédiatement avant l’affectation ( §16 (Affectation définitive) ).


Explication

Revenons maintenant à votre exemple, légèrement modifié:

 public static void main(Ssortingng[] args) { System.out.println("After: " + X); } private static final long X = assign(); private static long assign() { // Access the value before first assignment System.out.println("Before: " + X); return X + 1; } 

Il produit

 Before: 0 After: 1 

Rappelez-vous ce que nous avons appris. A l’intérieur de la méthode, la variable X n’a pas encore de valeur. Par conséquent, il pointe vers sa valeur par défaut car il s’agit d’une variable de classe et, selon le JLS, ces variables indiquent toujours immédiatement leurs valeurs par défaut (contrairement aux variables locales). Après la méthode d’assignation, la variable X se voit atsortingbuer la valeur 1 et à cause de la final nous ne pouvons plus la changer. Donc, ce qui suit ne fonctionnerait pas en raison de la final :

 private static long assign() { // Assign X X = 1; // Second assign after method will crash return X + 1; } 

Exemple dans le JLS

Grâce à @Andrew, j’ai trouvé un paragraphe JLS qui couvre exactement ce scénario, il le démontre également.

Mais d’abord, regardons

 private static final long X = X + 1; // Comstack-time error: // self-reference in initializer 

Pourquoi est-ce interdit, alors que l’access à la méthode est? Jetez un coup d’oeil au §8.3.3 qui parle des ressortingctions d’access aux champs si le champ n’a pas encore été initialisé.

Il répertorie certaines règles pertinentes pour les variables de classe:

Pour une référence par nom simple à une variable de classe f déclarée en classe ou interface C , il s’agit d’une erreur de compilation si :

  • La référence apparaît soit dans un initialiseur de variable de classe de C soit dans un initialiseur statique de C ( §8.7 ); et

  • La référence apparaît soit dans l’initialiseur du propre déclarant de f , soit à un point situé à gauche du déclarant de f ; et

  • La référence ne se trouve pas sur le côté gauche d’une expression d’affectation ( §15.26 ); et

  • La classe ou l’interface la plus interne entourant la référence est C

C’est simple, le X = X + 1 est intercepté par ces règles, la méthode n’y accède pas. Ils listent même ce scénario et donnent un exemple:

Les access par méthodes ne sont pas vérifiés de cette manière, donc:

 class Z { static int peek() { return j; } static int i = peek(); static int j = 1; } class Test { public static void main(Ssortingng[] args) { System.out.println(Zi); } } 

produit la sortie:

 0 

car la variable initializer for i utilise la méthode de la classe peek pour accéder à la valeur de la variable j avant que j ait été initialisée par sa variable initializer, auquel cas elle a toujours sa valeur par défaut ( §4.12.5 ).

Rien à voir avec la finale ici.

Comme il est au niveau de l’instance ou de la classe, il contient la valeur par défaut si rien n’est encore atsortingbué. C’est la raison pour laquelle vous voyez 0 lorsque vous y accédez sans assigner.

Si vous accédez à X sans affecter complètement, il contient les valeurs par défaut de long, qui est 0 , d’où les résultats.

Pas un bug.

Lorsque le premier appel à l’ scale est appelé depuis

 private static final long X = scale(10); 

Il essaie d’évaluer la return X * value . Une valeur n’a pas encore été atsortingbuée à X et la valeur par défaut d’un long est donc utilisée ( 0 ).

Donc cette ligne de code évalue à X * 10 soit 0 * 10 ce qui est 0 .

Ce n’est pas un bug du tout, en termes simples, ce n’est pas une forme illégale de références en aval, rien de plus.

 Ssortingng x = y; Ssortingng y = "a"; // this will not comstack Ssortingng x = getIt(); // this will comstack, but will be null Ssortingng y = "a"; public Ssortingng getIt(){ return y; } 

C’est simplement autorisé par la spécification.

Pour prendre votre exemple, c’est exactement là que cela correspond:

 private static final long X = scale(10) + 3; 

Vous faites une référence en scale à une scale qui n’est pas illégale de quelque manière que ce soit, mais vous permet d’obtenir la valeur par défaut de X encore une fois, cela est autorisé par le Spec (pour être plus précis, ce n’est pas interdit), donc ça marche très bien

Les membres de niveau classe peuvent être initialisés dans du code dans la définition de classe. Le bytecode compilé ne peut pas initialiser les membres de classe en ligne. (Les membres de l’instance sont traités de la même manière, mais cela n’est pas pertinent pour la question fournie.)

Quand on écrit quelque chose comme ceci:

 public class Demo1 { private static final long DemoLong1 = 1000; } 

Le bytecode généré serait similaire à celui-ci:

 public class Demo2 { private static final long DemoLong2; static { DemoLong2 = 1000; } } 

Le code d’initialisation est placé dans un initialiseur statique qui est exécuté lorsque le chargeur de classe charge la classe pour la première fois. Avec cette connaissance, votre échantillon original serait similaire à ce qui suit:

 public class RecursiveStatic { private static final long X; private static long scale(long value) { return X * value; } static { X = scale(10); } public static void main(Ssortingng[] args) { System.out.println(scale(5)); } } 
  1. La machine virtuelle Java charge RecursiveStatic en tant que point d’entrée du fichier jar.
  2. Le chargeur de classe exécute l’initialiseur statique lorsque la définition de classe est chargée.
  3. L’initialiseur appelle l’ scale(10) fonction scale(10) pour affecter le static final X
  4. La fonction scale(long) s’exécute pendant que la classe est partiellement initialisée en lisant la valeur non initialisée de X qui est la valeur par défaut de long ou 0.
  5. La valeur 0 * 10 est affectée à X et le chargeur de classe se termine.
  6. La machine virtuelle Java exécute l’ scale(5) appel de la méthode principale vide statique publique scale(5) qui multiplie 5 par la valeur X maintenant initialisée de 0 et renvoyant 0.

Le champ final statique X est affecté une seule fois, préservant la garantie détenue par le mot-clé final . Pour la requête ultérieure d’append 3 dans l’affectation, l’étape 5 ci-dessus devient l’évaluation de 0 * 10 + 3 qui est la valeur 3 et la méthode principale affichera le résultat de 3 * 5 qui correspond à la valeur 15 .

La lecture d’un champ non initialisé d’un object doit entraîner une erreur de compilation. Malheureusement pour Java, ce n’est pas le cas.

Je pense que la raison fondamentale pour laquelle ceci est le cas est “cachée” profondément dans la définition de la façon dont les objects sont instanciés et construits, bien que je ne connaisse pas les détails de la norme.

En un sens, la finale est mal définie car elle n’atteint même pas son objective déclaré en raison de ce problème. Cependant, si toutes vos classes sont correctement écrites, vous n’avez pas ce problème. Ce qui signifie que tous les champs sont toujours définis dans tous les constructeurs et qu’aucun object n’est jamais créé sans appeler l’un de ses constructeurs. Cela semble naturel jusqu’à ce que vous deviez utiliser une bibliothèque de sérialisation.