55 votes

Pourquoi la tentative d'imprimer une variable non initialisée ne donne pas toujours lieu à un message d'erreur

Certains peuvent trouver cela similaire à la question du SO Les variables Java Final auront-elles des valeurs par défaut ? mais cette réponse ne résout pas complètement le problème, car cette question n'imprime pas directement la valeur de x dans le bloc initialisateur d'instance.

Le problème survient lorsque j'essaie d'imprimer x directement à l'intérieur du bloc initialisateur d'instance, tout en ayant assigné une valeur à x avant la fin du bloc :

Cas 1

class HelloWorld {

    final int x;

    {
        System.out.println(x);
        x = 7;
        System.out.println(x);    
    }

    HelloWorld() {
        System.out.println("hi");
    }

    public static void main(String[] args) {
        HelloWorld t = new HelloWorld();
    }
}

Cela donne une erreur de compilation indiquant que la variable x pourrait ne pas avoir été initialisée.

$ javac HelloWorld.java
HelloWorld.java:6: error: variable x might not have been initialized
        System.out.println(x);
                           ^
1 error

Cas 2

Au lieu d'imprimer directement, j'appelle une fonction pour imprimer :

class HelloWorld {

    final int x;

    {
        printX();
        x = 7;
        printX();
    }

    HelloWorld() {
        System.out.println("hi");
    }

    void printX() {
        System.out.println(x);
    }

    public static void main(String[] args) {
        HelloWorld t = new HelloWorld();
    }
}

Cela compile correctement et donne une sortie

0
7
hi

Quelle est la différence conceptuelle entre les deux cas ?

32voto

kocko Points 18585

Dans le JLS, §8.3.3. Références directes pendant l'initialisation du champ il est indiqué qu'il y a une erreur de compilation lorsque :

L'utilisation de variables d'instance dont les déclarations apparaissent textuellement après l'utilisation est parfois limitée, même si ces variables d'instance sont dans la portée. Plus précisément, il s'agit d'une erreur de compilation si toutes les conditions suivantes sont remplies : 1. suivantes sont vraies :

  • La déclaration d'une variable d'instance dans une classe ou une interface C apparaît textuellement après une utilisation de la variable d'instance ;

  • L'utilisation est un simple nom dans un initialisateur de variable d'instance de C ou un initialisateur d'instance de C ;

  • L'utilisation ne se trouve pas sur le côté gauche d'une affectation ;

  • C est la classe ou l'interface la plus interne qui englobe l'utilisation.

Les règles suivantes sont accompagnées de quelques exemples, dont le plus proche du vôtre est celui-ci :

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

Les accès [aux variables statiques ou d'instance] par ne sont pas vérifiées de cette manière Ainsi, le code ci-dessus produit un résultat 0 car l'initialisateur de variable 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 son initialisateur de variable, auquel point il a encore sa valeur par défaut ( §4.12.5 Valeurs initiales des variables ).

Donc, pour résumer, votre deuxième exemple se compile et s'exécute bien, parce que le compilateur ne vérifie pas si l'option x a déjà été initialisée lorsque vous avez invoqué printX() et quand printX() a effectivement lieu au moment de l'exécution, le x sera assignée avec sa valeur par défaut ( 0 ).

12voto

Tunaki Points 2663

En lisant le JLS, la réponse semble être dans section 16.2.2 :

Un blanc final champ de membre V est définitivement assigné (et de plus n'est pas définitivement désassigné) avant le bloc (§14.2) qui est le corps de toute méthode dans la portée de V et avant la déclaration de toute classe déclarée dans la portée de l'élément V .

Cela signifie que lorsqu'une méthode est appelée, le champ final est affecté à sa valeur par défaut 0 avant d'être invoqué, de sorte que lorsque vous le référencez à l'intérieur de la méthode, celle-ci se compile avec succès et imprime la valeur 0.

Cependant, lorsque vous accédez au champ en dehors d'une méthode, il est considéré comme non attribué, d'où l'erreur de compilation. Le code suivant ne compilera pas non plus :

public class Main {
    final int x;
    {
        method();
        System.out.println(x);
        x = 7;
    }
    void method() { }
    public static void main(String[] args) { }
}

parce que :

  • V est [non]attribué avant toute autre déclaration S du bloc si V est [non]attribué après la déclaration qui précède immédiatement S dans le bloc.

Puisque le champ final x est non assigné avant l'invocation de la méthode, il l'est toujours après.

Cette note dans le JLS est également pertinente :

Notez qu'il n'existe aucune règle qui nous permettrait de conclure que V est définitivement non assigné avant le bloc qui est le corps de tout constructeur, méthode, initialisateur d'instance ou initialisateur statique déclaré dans l'élément C . Nous pouvons conclure de manière informelle que V n'est pas définitivement désaffecté avant le bloc qui est le corps de tout constructeur, méthode, initialisateur d'instance ou initialisateur statique déclaré en C, mais il n'est pas nécessaire qu'une telle règle soit énoncée explicitement.

4voto

Ok, voici mes deux cents.

Nous savons tous que les variables finales ne peuvent être initialisées que lors de la déclaration ou plus tard dans les constructeurs. En gardant ce fait à l'esprit, voyons ce qui s'est passé ici jusqu'à présent.

Aucune erreur Cas :

Ainsi, lorsque vous utilisez une méthode, elle a déjà une valeur.

 1) If you initialize it, that value.
 2) If not, the default value of data type. 

Cas d'erreur :

Lorsque vous faites cela dans un bloc d'initialisation, que vous voyez des erreurs.

Si vous regardez le docs of initialization block

{
    // whatever code is needed for initialization goes here
}

et

Le compilateur Java copie les blocs d'initialisation dans chaque constructeur. Cette approche peut donc être utilisée pour partager un bloc de code entre plusieurs constructeurs.

Dans l'œil du compilateur, votre code est littéralement égal à

class HelloWorld {

    final int x;
    HelloWorld() {
        System.out.println(x);  ------------ ERROR here obviously
        x = 7;
        System.out.println(x);  
        System.out.println("hi");
    }

    public static void main(String[] args) {
        HelloWorld t = new HelloWorld();
    }
}

Vous l'utilisez avant même de l'initialiser.

4voto

k0ner Points 672

La différence est que dans le premier cas, vous appelez System.out.println de bloc initialisateur donc le bloc qui est invoqué avant le constructeur. Dans la première ligne

System.out.println(x);

variable x n'est pas encore initialisé de sorte que vous obtenez une erreur de compilation.

Mais dans le second cas, vous appelez la méthode d'instance qui ne sait pas si la variable a déjà été initialisée, donc vous n'avez pas d'erreur de compilation et vous pouvez voir la valeur par défaut de la méthode d'instance. x

1voto

vishal gajera Points 1606

Cas 1 :

Donne une erreur de compilation,

Parce qu'à System.out.println(x);

vous essayez d'imprimer x qui n'a jamais été initialisé.

Cas 2 :

Cela fonctionne parce que vous n'utilisez pas directement des valeurs littérales, mais vous appelez une méthode, ce qui est correct.

La règle générale est la suivante ,

Si vous essayez d'accéder à une variable qui n'est jamais initialisée alors il donnera une erreur de compilation.

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