web-dev-qa-db-fra.com

Le final est-il mal défini?

Tout d'abord, un casse-tête: 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:

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 ultérieurement sur 3. Ceci est une violation de final!

Le modificateur statique, en combinaison avec le modificateur final, est également utilisé pour définir les constantes. Le dernier modificateur indique que la valeur de ce champ ne peut pas être modifiée .

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


Ma question: Est-ce un bug? final est-il mal défini?


Voici le code qui m'intéresse. _ Deux valeurs différentes sont attribuées à X: 0 et 3. Je crois que ceci 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 un doublon possible de ordre d'initialisation du champ final statique Java . Je crois 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 combinée à la final tag. À partir de l’autre question, je ne saurais comprendre pourquoi le code de ma question ne fait pas une erreur.

Ceci est particulièrement clair en regardant le résultat obtenu par ernesto: lorsque a est étiqueté avec final, il obtient le résultat suivant:

a=5
a=5

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

183
Little Helper

Une découverte très intéressante. Pour le comprendre, nous devons creuser dans la spécification de langage Java ( JLS ).

La raison en est que final n'autorise qu'un affectation. Cependant, la valeur par défaut est no affectation. En fait, chaque telle variable (variable de classe, variable d'instance, composant de tableau) pointe sur sa valeur par défaut ​​depuis le début, avant assignments. La première affectation change alors la référence.


Variables de classe et valeur par défaut

Regardez l'exemple suivant:

private static Object x;

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

Nous n'avons pas explicitement assigné de valeur à x, bien qu'elle pointe sur null, il s'agit de la valeur par défaut. Comparez cela à §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 quand il est créé ( §15.9 , §15.10.2 )

Notez que cela n’est valable 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(String[] args) {
    Object x;
    System.out.println(x);
    // Compile-time error:
    // variable x might not have been initialized
}

Du même paragraphe JLS:

Une variable locale ( §14.4 , §14.14 ) doit être explicitement donné une valeur avant son utilisation, par initialisation ( §14.4 ) ou par assignation ( §15.26 ), dans une manière qui peut être vérifiée en utilisant les règles pour une affectation définie ( §16 (Assignation Définie)) ).


Variables finales

Voyons maintenant final, de §4.12.4 :

final Variables

Une variable peut être déclarée final. Une variable finale ne peut être affectée qu'une seule fois . C’est 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 (Assignation définitive) ).


Explication

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

public static void main(String[] 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 sort

Before: 0
After: 1

Rappelez-vous ce que nous avons appris. À l'intérieur de la méthode assign la variable X n'était pas affectée à une valeur encore. 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 (par opposition à variables locales). Après la méthode assign, la valeur 1 est affectée à la variable X et, à cause de final, nous ne pouvons plus la modifier. Donc, ce qui suit ne fonctionnerait pas à cause de 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, jetons un coup d'oeil à

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

Pourquoi n'est-ce pas autorisé alors que l'accès depuis la méthode est? Jetez un coup d'œil à §8.3. qui parle du moment où les accès aux champs sont restreints si le champ n'a pas encore été initialisé.

Il énumère quelques règles pertinentes pour les variables de classe:

Pour une référence par un nom simple à une variable de classe f déclarée dans une classe ou une 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éclarateur de f, soit en un point situé à gauche du déclarateur de f; et

  • La référence ne figure pas à gauche d'une expression d'affectation ( §15.26 ); et

  • La classe ou l'interface la plus interne qui entoure la référence est C.

C'est simple, le X = X + 1 est capturé par ces règles, l'accès à la méthode ne l'est pas. Ils listent même ce scénario et donnent un exemple:

Les accès par méthodes ne sont pas vérifiés de cette façon, donc:

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

produit la sortie:

0

parce que la variable d’initialisation pour i utilise la méthode de classe peek pour accéder à la valeur de la variable j avant que j ne soit initialisé par son initialiseur de variable, auquel elle a toujours sa valeur par défaut ( §4.12.5 ).

215
Zabuza

Rien à voir avec la finale ici.

Puisqu'il se situe au niveau de l'instance ou de la classe, il conserve 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 conservera les valeurs par défaut de long qui sont 0, d'où les résultats.

22
Suresh Atta

Pas un bug.

Lorsque le premier appel à scale est appelé depuis

private static final long X = scale(10);

Il essaie d'évaluer return X * value. X n'a pas encore reçu de valeur. Par conséquent, la valeur par défaut d'un long est utilisée (c'est-à-dire 0).

Donc, cette ligne de code est évaluée à X * 10 c'est-à-dire 0 * 10 qui est 0.

20
OldCurmudgeon

Ce n'est pas du tout un bug, simplement, ce n'est pas un forme illégale de références en aval, rien de plus.

String x = y;
String y = "a"; // this will not compile 


String x = getIt(); // this will compile, but will be null
String y = "a";

public String getIt(){
    return y;
}

C'est simplement permis par la spécification.

Pour prendre votre exemple, c'est exactement où cela correspond:

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

Vous faites un référence vers l'avant à scale qui n'est pas illégal comme cela a été dit précédemment, mais vous permet d'obtenir la valeur par défaut de X. encore une fois, cela est autorisé par la spécification (pour être plus précis, il n'est pas interdit), donc cela fonctionne très bien

14
Eugene

Les membres au niveau de la classe peuvent être initialisés sous forme de code dans la définition de la classe. Le bytecode compilé ne peut pas initialiser les membres de la classe en ligne. (Les membres d'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:

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 classes charge la classe pour la première fois. Avec cette connaissance, votre échantillon d'origine serait semblable au suivant:

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(String[] args) {
        System.out.println(scale(5));
    }
}
  1. La JVM charge RecursiveStatic en tant que point d'entrée du jar.
  2. Le chargeur de classe exécute l'initialiseur statique lorsque la définition de classe est chargée.
  3. L’initialiseur appelle la fonction scale(10) pour attribuer le champ static finalX.
  4. La fonction scale(long) s'exécute alors 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 de 0 * 10 est affectée à X et le chargeur de classes est terminé.
  6. La machine virtuelle Java exécute la méthode principale publique statique statique appelant scale(5), qui multiplie 5 par la valeur X maintenant initialisée de 0, qui renvoie 0.

Le champ final statique X n'est attribué qu'une seule fois, tout en préservant la garantie détenue par le mot clé final. Pour la requête suivante d'ajout de 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 est la valeur. 15.

4
psaxton

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" au fond de la définition de la manière dont les objets sont instanciés et construits, bien que je ne connaisse pas les détails de la norme.

Dans un sens, le terme final est mal défini car il n'atteint même pas le but recherché par 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.

3
Kafein