Tout d'abord, un puzzle: qu'est-ce que le code suivant imprime?
public class RecursiveStatic {
public static void main(String[] 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
à l'échelle (longue) et redéfinissez X = scale(10) + 3
, les impressions seront X = 0
alors X = 3
. Cela signifie qu'il X
est temporairement défini sur 0
et ultérieurement défini sur 3
. Ceci est une violation de final
!
Le modificateur statique, en combinaison avec le modificateur final, est également utilisé pour définir des constantes. Le modificateur final indique que la valeur de ce champ ne peut pas changer .
Source: https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [italiques ajoutés]
Ma question: est-ce un bug? Est final
-ce mal défini?
Voici le code qui m'intéresse.
X
Deux valeurs différentes sont attribuées: 0
et 3
. Je pense que c'est une violation de final
.
public class RecursiveStatic {
public static void main(String[] 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 une duplication possible de l'ordre d'initialisation final des champs statiques Java . Je crois que cette question n'est pas un doublon puisque l'autre question porte sur l'ordre d'initialisation tandis que ma question porte sur une initialisation cyclique combinée avec la final
balise. De la seule autre question, je ne pourrais pas comprendre pourquoi le code de ma question ne fait pas d'erreur.
Ceci est particulièrement clair en regardant la sortie qu'ernesto obtient: lorsqu'il a
est étiqueté final
, il obtient la sortie suivante:
a=5
a=5
ce qui n'implique pas l'essentiel de ma question: comment une final
variable change-t-elle sa variable?
la source
X
membre revient à faire référence à un membre de sous-classe avant que le constructeur de la super classe n'ait fini, c'est votre problème et non la définition definal
.A blank final instance variable must be definitely assigned (§16.9) at the end of every constructor (§8.8) of the class in which it is declared; otherwise a compile-time error occurs.
Réponses:
Une trouvaille très intéressante. Pour le comprendre, nous devons creuser dans la spécification du langage Java ( JLS ).
La raison en est que
final
n'autorise qu'une seule affectation . Cependant, la valeur par défaut est aucune affectation . En fait, chacune de ces variables ( 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 ensuite la référence.Variables de classe et valeur par défaut
Jetez un œil à l'exemple suivant:
Nous n'avons pas explicitement attribué de valeur à
x
, bien qu'elle pointe versnull
, c'est la valeur par défaut. Comparez cela au §4.12.5 :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:
Du même paragraphe JLS:
Variables finales
Maintenant, nous regardons
final
, à partir du §4.12.4 :Explication
Revenons maintenant à votre exemple, légèrement modifié:
Il sort
Rappelez-vous ce que nous avons appris. À l'intérieur de la méthode, aucune valeur n'a encore été attribuée à
assign
la variable . Par conséquent, il pointe vers sa valeur par défaut puisqu'il s'agit d'une variable de classe et, selon le JLS, ces variables pointent toujours immédiatement vers leurs valeurs par défaut (contrairement aux variables locales). Après la méthode, la valeur est attribuée à la variable et à cause de cela, nous ne pouvons plus la modifier. Ainsi, les éléments suivants ne fonctionneraient pas en raison de :X
assign
X
1
final
final
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 regardons d'abord
Pourquoi n'est-ce pas autorisé, alors que l'accès à partir de la méthode l'est? Jetez un œil au §8.3.3 qui parle du moment où les accès aux champs sont restreints si le champ n'a pas encore été initialisé.
Il répertorie quelques règles pertinentes pour les variables de classe:
C'est simple, le
X = X + 1
est pris par ces règles, l'accès à la méthode non. Ils énumèrent même ce scénario et donnent un exemple:la source
X
partir de la méthode. Cela ne me dérangerait pas beaucoup. Cela dépend simplement de la façon dont le JLS définit exactement les choses pour fonctionner en détail. Je n'utiliserais jamais un code comme celui-là, il exploite simplement certaines règles du JLS.forwards references
(qui font également partie du JLS). c'est si simple sans cette longue réponse stackoverflow.com/a/49371279/1059372Rien à voir avec la finale ici.
Puisqu'il est au niveau de l'instance ou de la classe, il contient la valeur par défaut si rien n'est encore attribué. C'est la raison pour laquelle vous voyez
0
lorsque vous y accédez sans attribuer.Si vous accédez
X
sans attribuer complètement, il contient les valeurs par défaut de long0
, d'où les résultats.la source
Pas un bug.
Lorsque le premier appel à
scale
est appelé deIl essaie d'évaluer
return X * value
.X
n'a pas encore reçu de valeur et, par conséquent, la valeur par défaut de along
est utilisée (ce qui est0
).Donc, cette ligne de code évalue à
X * 10
ie0 * 10
qui est0
.la source
X = scale(10) + 3
. DepuisX
, lorsqu'il est référencé à partir de la méthode, est0
. Mais après ça l'est3
. OP pense donc queX
deux valeurs différentes sont attribuées à lafinal
.return X * value
.X
long
0
N'a pas encore reçu de valeur et prend donc la valeur par défaut pour un qui est . "? Il n'est pas dit qu'il aX
la valeur par défaut mais que leX
est "remplacé" (veuillez ne pas citer ce terme;)) par la valeur par défaut.Ce n'est pas du tout un bogue, ce n'est simplement pas une forme illégale de références directes, rien de plus.
C'est simplement autorisé par la spécification.
Pour prendre votre exemple, c'est exactement là que cela correspond:
Vous faites une référence directe à
scale
cela n'est en aucun cas illégale comme indiqué précédemment, mais vous permet d'obtenir la valeur par défaut deX
. encore une fois, cela est autorisé par la spécification (pour être plus exact, ce n'est pas interdit), donc cela fonctionne très bienla source
Les membres de niveau de classe peuvent être initialisés dans le code dans la définition de classe. Le bytecode compilé ne peut pas initialiser les membres de la 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 ce qui suit:
Le bytecode généré serait similaire à ce qui suit:
Le code d'initialisation est placé dans un initialiseur statique qui est exécuté lorsque le chargeur de classe charge pour la première fois la classe. Avec cette connaissance, votre échantillon d'origine serait similaire à ce qui suit:
scale(10)
pour affecter lestatic final
champX
.scale(long)
fonction s'exécute pendant que la classe est partiellement initialisée en lisant la valeur non initialiséeX
dont la valeur par défaut est long ou 0.0 * 10
est attribuée àX
et le chargeur de classe se termine.scale(5)
qui multiplie 5 par laX
valeur désormais initialisée de 0 et renvoie 0.Le champ final statique
X
n'est attribué qu'une seule fois, en préservant la garantie détenue par lefinal
mot - clé. Pour la requête suivante d'ajout de 3 dans l'affectation, l'étape 5 ci-dessus devient l'évaluation0 * 10 + 3
dont est la valeur3
et la méthode principale imprimera le résultat3 * 5
dont est la valeur15
.la source
La lecture d'un champ non initialisé d'un objet devrait entraîner une erreur de compilation. Malheureusement pour Java, ce n'est pas le cas.
Je pense que la raison fondamentale pour laquelle c'est le cas est «cachée» profondément dans la définition de la façon dont les objets sont instanciés et construits, bien que je ne connaisse pas les détails de la norme.
En un sens, final est mal défini car il n'accomplit même pas son objectif déclaré en raison de ce problème. Cependant, si toutes vos classes sont correctement écrites, vous n'avez pas ce problème. Cela signifie que tous les champs sont toujours définis dans tous les constructeurs et qu'aucun objet 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.
la source