web-dev-qa-db-fra.com

Pourquoi les variables locales ne sont-elles pas initialisées en Java?

Existe-t-il une raison pour laquelle les concepteurs de Java ont estimé que les valeurs locales ne devraient pas recevoir de valeur par défaut? Sérieusement, si une valeur par défaut peut être attribuée aux variables d'instance, pourquoi ne pouvons-nous pas en faire autant pour les variables locales?

Et cela pose aussi des problèmes, comme expliqué dans ce commentaire sur un article de blog :

Cette règle est particulièrement frustrante lorsque vous essayez de fermer une ressource dans un bloc enfin. Si j'instancie la ressource dans un essai, mais que j'essaie de la fermer dans le dernier cas, j'obtiens cette erreur. Si je déplace l'instanciation en dehors de l'essai, j'obtiens une autre erreur indiquant qu'il doit y avoir un essai.

Très frustrant.

91
Shivasubramanian A

Les variables locales sont principalement déclarées pour faire des calculs. C'est donc la décision du programmeur de définir la valeur de la variable et il ne doit pas prendre de valeur par défaut. Si le programmeur, par erreur, n'a pas initialisé une variable locale et qu'elle prend la valeur par défaut, la sortie pourrait être une valeur inattendue. Ainsi, dans le cas de variables locales, le compilateur demandera au programmeur d’initialiser avec une valeur avant d’accéder à la variable afin d’éviter l’utilisation de valeurs non définies.

55
Warrior

Le "problème" auquel vous associez semble décrire cette situation:

SomeObject so;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  so.CleanUp(); // Compiler error here
}

Le commentateur se plaint du fait que le compilateur hésite sur la ligne de la section finally, affirmant que so pourrait ne pas être initialisé. Le commentaire mentionne ensuite une autre façon d'écrire le code, probablement quelque chose comme ceci:

// Do some work here ...
SomeObject so = new SomeObject();
try {
  so.DoUsefulThings();
} finally {
  so.CleanUp();
}

Le commentateur n'est pas satisfait de cette solution car le compilateur dit alors que le code "doit être essayé". Je suppose que cela signifie qu'une partie du code peut générer une exception qui n'est plus gérée. Je ne suis pas sûr. Aucune des deux versions de mon code ne gère d’exception. Par conséquent, tout élément lié à une exception dans la première version doit fonctionner de la même manière dans la seconde.

Quoi qu'il en soit, cette deuxième version de code est le moyen correct de l'écrire. Dans la première version, le message d'erreur du compilateur était correct. La variable so peut ne pas être initialisée. En particulier, si le constructeur SomeObject échoue, so ne sera pas initialisé et il sera donc erroné d'essayer d'appeler so.CleanUp. Entrez toujours la section tryaprès que vous avez acquis la ressource que la section finally finalise.

Le bloc try-finally après l'initialisation so est là seulement pour protéger l'instance SomeObject, afin de s'assurer qu'elle est nettoyée quoi qu'il se passe. S'il y a autres choses à exécuter, mais qu'elles ne soient pas liées au fait que l'instance SomeObject était une propriété allouée, elles devraient alors être placées dans le bloc anothertry-finally, probablement celui qui enveloppe J'ai montré.

Exiger que les variables soient assignées manuellement avant utilisation ne pose pas de problèmes réels. Cela ne conduit qu'à des tracas mineurs, mais votre code sera meilleur pour cela. Vous aurez des variables avec une portée plus limitée et des blocs try-finally qui n'essayeront pas de protéger trop.

Si les variables locales avaient des valeurs par défaut, alors so dans le premier exemple aurait été null. Cela n'aurait vraiment rien résolu. Au lieu d’obtenir une erreur de compilation dans le bloc finally, vous auriez une NullPointerException masquée qui pourrait cacher toute autre exception susceptible de se produire dans la section du code "Effectuer des travaux ici". (Ou les exceptions dans les sections finally sont-elles automatiquement reliées à l’exception précédente? Je ne me souviens pas. Même dans ce cas, vous auriez une exception supplémentaire sous la forme de la vraie.)

22
Rob Kennedy

De plus, dans l'exemple ci-dessous, une exception peut avoir été levée à l'intérieur de la construction SomeObject, auquel cas la variable 'so' serait nulle et l'appel à CleanUp lève une exception NullPointerException

SomeObject so;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  so.CleanUp(); // Compiler error here
}

Voici ce que j'ai tendance à faire:

SomeObject so = null;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  if (so != null) {
     so.CleanUp(); // safe
  }
}
12
Electric Monk

Notez que les dernières variables instance/membre ne sont pas initialisées par défaut. Parce que ceux-ci sont définitifs et ne peuvent plus être modifiés dans le programme par la suite. C'est la raison pour laquelle Java ne leur donne aucune valeur par défaut et ne force pas le programmeur à l'initialiser. 

D'autre part, les variables de membre non final peuvent être modifiées ultérieurement. Par conséquent, le compilateur ne les laisse pas rester non initialisés, précisément, car ils peuvent être modifiés ultérieurement. En ce qui concerne les variables locales, la portée des variables locales est beaucoup plus étroite. Le compilateur sait quand il s'habitue. Par conséquent, obliger le programmeur à initialiser la variable a du sens.

10
Adeel Ansari

La réponse réelle à votre question vient du fait que les variables de méthode sont instanciées en ajoutant simplement un nombre au pointeur de la pile. Les mettre à zéro serait une étape supplémentaire. Pour les variables de classe, elles sont mises en mémoire initialisée sur le tas.

Pourquoi ne pas faire le pas supplémentaire? Faites un pas en arrière - Personne n'a mentionné que "l'avertissement" dans ce cas est une très bonne chose.

Vous ne devriez jamais initialiser votre variable à zéro ou à zéro lors du premier passage (lorsque vous le codez pour la première fois). Attribuez-le à la valeur réelle ou ne l'attribuez pas du tout, car si ce n'est pas le cas, Java peut vous avertir lorsque vous vous trompez. Prenez la réponse de Electric Monk comme un bon exemple. Dans le premier cas, il est extrêmement utile de vous dire que si try () échoue parce que le constructeur de SomeObject a lancé une exception, vous vous retrouveriez avec un NPE dans le fichier finally. Si le constructeur ne peut pas lancer une exception, cela ne devrait pas être tenté.

Cet avertissement est un excellent vérificateur de programmeurs multi-chemins génial qui m'a évité de faire des choses stupides, car il vérifie chaque chemin et s'assure que si vous utilisiez la variable dans un chemin, vous deviez l'initialiser dans chaque chemin qui y conduit . À présent, je n’initialise jamais explicitement les variables tant que je n’ai pas déterminé que c’est la bonne chose à faire.

En plus de cela, ne vaut-il pas mieux de dire explicitement "int size = 0" plutôt que "int size" et de faire en sorte que le prochain programmeur comprenne que vous voulez qu'il soit nul?

D'un autre côté, je ne peux pas trouver une seule raison valable pour que le compilateur initialise toutes les variables non initialisées à 0.

9
Bill K

(Il peut sembler étrange de poster une nouvelle réponse si longtemps après la question, mais un duplicata est apparu.)

Pour moi, la raison se résume à ceci: Le but des variables locales est différent de celui des variables d'instance. Les variables locales sont là pour être utilisées dans le cadre d'un calcul; les variables d'instance sont là pour contenir l'état. Si vous utilisez une variable locale sans lui attribuer de valeur, c'est probablement une erreur de logique.

Cela dit, je pouvais totalement prendre du retard en exigeant que les variables d'instance soient toujours explicitement initialisées; l'erreur se produirait sur n'importe quel constructeur où le résultat autorise une variable d'instance non initialisée (par exemple, non initialisée à la déclaration et non dans le constructeur). Mais ce n'est pas la décision Gosling, et. al., a pris au début des années 90, alors nous sommes ici. (Et je ne dis pas qu'ils ont fait le mauvais appel.)

Je pourrais pas me mettre derrière les variables locales par défaut. Oui, nous ne devrions pas nous fier aux compilateurs pour revérifier notre logique, mais ce n’est pas le cas, mais cela reste pratique lorsque le compilateur en attrape un. :-)

4
T.J. Crowder

Je pense que l'objectif principal était de maintenir la similitude avec C/C++. Cependant, le compilateur détecte et vous avertit de l'utilisation de variables non initialisées, ce qui réduira le problème à un minimum. Du point de vue des performances, il est un peu plus rapide de vous permettre de déclarer des variables non initialisées car le compilateur n'aura pas à écrire une instruction d'affectation, même si vous écrasez la valeur de la variable dans l'instruction suivante.

3
Mehrdad Afshari

Il est plus efficace de ne pas initialiser les variables et, dans le cas de variables locales, il est prudent de le faire, car l'initialisation peut être suivie par le compilateur.

Dans les cas où vous avez besoin d'une variable à initialiser, vous pouvez toujours le faire vous-même. Ce n'est donc pas un problème.

2
starblue

L'idée derrière les variables locales est qu'elles n'existent que dans la portée limitée pour laquelle elles sont nécessaires. En tant que tel, il ne devrait y avoir aucune raison d’incertitude quant à la valeur, ou du moins à l’origine de cette valeur. J'imaginais beaucoup d'erreurs résultant d'une valeur par défaut pour les variables locales.

Par exemple, considérons le code simple suivant ... (NB) supposons, à des fins de démonstration, que les valeurs locales sont affectées d'une valeur par défaut, comme spécifié, si elles ne sont pas explicitement initialisées

System.out.println("Enter grade");
int grade = new Scanner(System.in).nextInt(); //I won't bother with exception handling here, to cut down on lines.
char letterGrade; //let us assume the default value for a char is '\0'
if (grade >= 90)
    letterGrade = 'A';
else if (grade >= 80)
    letterGrade = 'B';
else if (grade >= 70)
    letterGrade = 'C';
else if (grade >= 60)
    letterGrade = 'D';
else
    letterGrade = 'F';
System.out.println("Your grade is " + letterGrade);

Quand tout est dit et fait, en supposant que le compilateur a attribué une valeur par défaut de '\ 0' à letterGrade, ce code, tel qu'écrit, fonctionnerait correctement. Cependant, si nous avons oublié la déclaration else?

Un test de notre code peut avoir les conséquences suivantes

Enter grade
43
Your grade is

Ce résultat, bien que prévisible, n’était sûrement pas l’intention du codeur. En effet, probablement dans une vaste majorité des cas (ou du moins dans un nombre significatif de ceux-ci), la valeur par défaut ne serait pas la valeur souhaitée, de sorte que dans la grande majorité des cas, la valeur par défaut serait la suivante en erreur. Il est plus judicieux de forcer le codeur à attribuer une valeur initiale à une variable locale avant de l’utiliser, car le problème de débogage causé par l’oubli du = 1 dans for(int i = 1; i < 10; i++) l'emporte de loin sur la commodité de ne pas avoir à inclure le = 0 dans for(int i; i < 10; i++).

Il est vrai que les blocs try-catch-finally pourraient devenir un peu désordonnés (mais ce n'est pas un catch-22 comme le suggère la citation), lorsqu'un objet lève par exemple une exception vérifiée dans son constructeur. raison ou une autre, quelque chose doit être fait à cet objet à la fin du bloc dans finalement. Un exemple parfait en est le traitement des ressources, qui doivent être fermées.

Une façon de gérer cela dans le passé pourrait être comme ça ...

Scanner s = null; //declared and initialized to null outside the block. This gives us the needed scope, and an initial value.
try {
    s = new Scanner(new FileInputStream(new File("filename.txt")));
    int someInt = s.nextInt();
} catch (InputMismatchException e) {
    System.out.println("Some error message");
} catch (IOException e) {
    System.out.println("different error message"); 
} finally {
    if (s != null) //in case exception during initialization prevents assignment of new non-null value to s.
        s.close();
}

Cependant, à partir de Java 7, ce bloc n'est finalement plus nécessaire avec try-with-resources, comme ça.

try (Scanner s = new Scanner(new FileInputStream(new File("filename.txt")))) {
...
...
} catch(IOException e) {
    System.out.println("different error message");
}

Cela dit, (comme son nom l'indique), cela ne fonctionne qu'avec des ressources.

Et bien que le premier exemple soit un peu dégueu, cela en dit peut-être plus sur la façon dont try-catch-finally ou ces classes sont implémentées que sur les variables locales et sur la manière dont elles sont implémentées.

Il est vrai que les champs sont initialisés à une valeur par défaut, mais c'est un peu différent. Lorsque vous dites, par exemple, int[] arr = new int[10];, dès que vous avez initialisé ce tableau, l'objet existe en mémoire à un emplacement donné. Supposons un instant qu'il n'y a pas de valeurs par défaut, mais que la valeur initiale correspond à la série de 1 et de 0 qui se trouve dans cet emplacement de mémoire pour le moment. Cela pourrait conduire à un comportement non déterministe dans un certain nombre de cas.

Supposons que nous ayons ...

int[] arr = new int[10];
if(arr[0] == 0)
    System.out.println("Same.");
else
    System.out.println("Not same.");

Il serait parfaitement possible que Same. soit affiché dans une exécution et Not same. dans une autre. Le problème pourrait devenir encore plus grave une fois que vous aurez commencé à parler des variables de référence.

String[] s = new String[5];

Selon la définition, chaque élément de s doit pointer vers une chaîne (ou est null). Toutefois, si la valeur initiale correspond à la série de 0 et de 1 qui se produit à cet emplacement de mémoire, non seulement il n’ya aucune garantie que vous obtiendrez les mêmes résultats à chaque fois, mais il n’existe pas non plus que les points de l'objet s [0] to (en supposant qu'il pointe vers quelque chose de significatif) même est une chaîne (c'est peut-être un lapin, : p)! Ce manque d’intérêt pour le type irait à l’encontre de tout ce qui fait Java Java. Ainsi, bien que les valeurs par défaut pour les variables locales puissent au mieux être considérées comme optionnelles, les valeurs par défaut pour les variables d'instance sont plus proches de la nécessité nécessité.

1
kumquatfelafel

Eclipse vous donne même des avertissements concernant les variables non initialisées, de sorte que cela devient assez évident de toute façon. Personnellement, je pense que c’est une bonne chose qu’il s’agisse du comportement par défaut, sinon votre application pourrait utiliser des valeurs inattendues. Au lieu de générer une erreur, le compilateur ne fera rien (mais peut-être donner un avertissement), puis vous gratterez vous vous demandez pourquoi certaines choses ne se comportent pas tout à fait comme il se doit.

0
Kezzer

Les variables locales sont stockées dans une pile, mais les variables d'instance sont stockées dans le segment de mémoire. Il est donc possible qu'une valeur précédente de la pile soit lue au lieu d'une valeur par défaut, comme cela se produit dans le segment de mémoire. Pour cette raison, jvm ne permet pas d'utiliser une variable locale sans l'initialiser.

0
David Santamaria

La variable d'instance aura des valeurs par défaut, mais les variables locales ne peuvent pas avoir de valeurs par défaut. Comme les variables locales sont essentiellement des méthodes/comportements, son objectif principal est de faire des opérations ou des calculs. Par conséquent, il n'est pas judicieux de définir des valeurs par défaut pour les variables locales. Sinon, il est très difficile et fastidieux de vérifier les raisons des réponses inattendues.

0
Xiaogang