187 votes

Le final est-il mal défini?

Tout d'abord, un casse-tête: Que fait le code suivant imprimer?

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éponse:

0

Spoilers ci-dessous.


Si vous imprimez X de l'échelle(de long) et de redéfinir X = scale(10) + 3, les impressions seront X = 0 alors X = 3. Cela signifie qu' X est temporairement mis à l' 0 et plus tard, à l' 3. C'est une violation de l' final!

Le modificateur static, 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 mal définis?


Voici le code qui m'intéresse. X est attribué à deux valeurs différentes: 0 et 3. Je pense que c'est une violation de l' 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é identifiée comme un double possible de Java static final de champ de l'ordre d'initialisation. Je crois que cette question n'est pas un doublon car l'autre question a trait à l'ordre de l'initialisation, tandis que ma question a trait à une reprise cyclique de l'initialisation du combiné avec l' finalbalise. De l'autre question, seul, je ne serais pas en mesure de comprendre pourquoi le code dans ma question n'est pas de faire une erreur.

Cela est particulièrement clair en regardant la sortie ernesto obtient: lors de l' a porte final, il obtient le résultat suivant:

a=5
a=5

ce qui n'implique pas la partie principale de ma question: Comment un final changement de variable variable?

218voto

Zabuza Points 11045

Un cas très intéressant de trouver. Pour le comprendre, nous avons besoin de creuser dans le Langage Java Specification (JLS).

La raison en est qu' final ne permet qu'une seule affectation. La valeur par défaut, cependant, n'est pas d'affectation. En fait, chaque variable (variable de classe, variable d'instance, de composants de la matrice) points à sa valeur par défaut depuis le début, avant les affectations. La première tâche puis change de la référence.


Variables de classe et de la valeur par défaut

Jetez un oeil à l'exemple suivant:

private static Object x;

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

Nous n'avons pas explicitement attribuer une valeur à l' x, mais il points de null, c'est la valeur par défaut. Comparez §4.12.5:

Valeurs initiales des Variables

Chaque variable de classe, variable d'instance, ou de composants de la matrice est initialisé avec une valeur par défaut lorsqu'il est créé (§15.9, §15.10.2)

Notez que cela ne tient que pour ce type de variables, comme dans notre exemple. Il n'est pas valable 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
}

De la même JLS paragraphe:

Une variable locale (§14.4, §14.14) doit être donné explicitement une valeur avant de l'utiliser, soit par l'initialisation (§14.4) ou de cession (§15.26), d'une manière qui peut être vérifié en utilisant les règles pour l'affectation définitive (§16 (Nette de Cession)).


Les variables Final

Maintenant, nous prenons un coup d'oeil à l' final, à partir du§4.12.4:

final Variables

Une variable peut être déclarée final. Une finale de la variable ne peut être attribué à la fois. C'est une erreur de compilation si une finale est attribué à la variable, sauf si elle est certainement non affecté immédiatement avant la cession (§16 (Nette de Cession)).


Explication

Maintenant revenir à votre exemple, légèrement modifiée:

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;
}

C'sorties

Before: 0
After: 1

Rappel de ce que nous avons appris. À l'intérieur de la méthode de assign la variable X a pas attribué une valeur encore. Par conséquent, il des points à sa valeur par défaut, car il est une variable de classe et selon la JLS ces variables toujours immédiatement le signaler à leurs valeurs par défaut (contrairement aux variables locales). Après l' assign méthode de la variable X est affectée de la valeur 1 , et en raison de final on ne peut pas changer plus. Donc la suite ne serait pas travailler en raison d' 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 JLS paragraphe qui couvre exactement ce scénario, il démontre également qu'il.

Mais d'abord, nous allons jeter un oeil à

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

Pourquoi ce n'est pas permis, alors que l'accès à partir de la méthode est? Regardez §8.3.3 qui parle lorsque les accès aux champs sont limités si le champ n'a pas encore été initialisé.

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

Pour une référence par le simple nom d'une variable de classe f déclarée dans la classe ou de l'interface C, c'est une erreur de compilation si:

  • La référence apparaît dans une variable de classe de l'initialiseur de C ou dans un initialiseur statique d' C (§8.7); et

  • La référence apparaît dans l'initialiseur de f's propre déclaration ou à un point vers la gauche de l' f's de demande de déclaration; et

  • La référence n'est pas sur le côté de main gauche d'une expression d'affectation (§15.26); et

  • Le plus profond de la classe ou de l'interface entourant la référence est - C.

C'est simple, l' X = X + 1 est pris par ces règles, la méthode d'accès pas. Ils ont même liste de ce scénario et de donner un exemple:

Accès par les méthodes ne sont pas contrôlé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 de la sortie:

0

parce que la variable d'initialiseur pour i utilise la méthode de la classe peek pour accéder à la valeur de la variable j avant j a été initialisé par la variable d'initialiseur, à quel point il a encore sa valeur par défaut (§4.12.5).

23voto

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 attribution.

Si vous accédez à X sans assigner complètement, il conserve les valeurs par défaut de long qui sont 0 , d'où les résultats.

20voto

OldCurmudgeon Points 16615

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 pour 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 .

14voto

Eugene Points 6271

Ce n'est pas un bug à tous, il suffit de mettre ce n'est pas une forme illégale de références vers l'avant, 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 tout simplement autorisés par le cahier des charges.

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

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

Vous faites une référence directe à l' scale qui n'est pas illégal en quelque sorte comme l'a dit avant, mais vous permet d'obtenir la valeur par défaut de X. encore une fois, cela est autorisé par les spécifications (pour être plus exact, il n'est pas interdit), de sorte qu'il fonctionne très bien

4voto

psaxton Points 111

Le niveau de la classe les membres peuvent être initialisées dans le code à l'intérieur de la définition de la classe. Le code compilé ne peut pas initialiser les membres de la classe en ligne. (Les membres de l'Instance sont traitées de la même façon, mais ce n'est pas pertinente pour la question fournis.)

Quand on écrit quelque chose comme:

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

Le bytecode généré serait semblable à la suivante:

public class Demo2 {
    private static final long DemoLong2;

    static {
        DemoLong2 = 1000;
    }
}

Le code d'initialisation est placé à l'intérieur d'un initialiseur statique qui est exécuté lorsque le chargeur de classe premier chargement de la classe. Avec cette connaissance, votre échantillon original serait semblable à la suivante:

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 le RecursiveStatic que le pot du point d'entrée.
  2. Le chargeur de classe exécute l'initialiseur statique lors de la définition de classe est chargé.
  3. L'initialiseur appelle la fonction scale(10) d'affecter l' static final domaine X.
  4. L' scale(long) fonction s'exécute alors que la classe est partiellement initialisé la lecture de la valeur non initialisée de X qui est la valeur par défaut de longue ou de 0.
  5. La valeur de 0 * 10 est attribué à l' X et le chargeur de classe complète.
  6. La machine exécute le public static void main l'appel de méthode scale(5) qui multiplie 5 par le présent initialisé X de la valeur de 0 renvoie 0.

La statique de la finale de champ X n'est attribué qu'une seule fois, en préservant la garantie détenue par l' final mot-clé. Pour la suite de la requête de l'addition de 3 dans l'attribution, à l'étape 5 ci-dessus devient l'évaluation de l' 0 * 10 + 3 qui est la valeur 3 et la principale méthode d'imprimer le résultat de l' 3 * 5 qui est la valeur 15.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X