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

103

Y avait-il une raison pour laquelle les concepteurs de Java estimaient que les variables locales ne devraient pas avoir de valeur par défaut? Sérieusement, si les variables d'instance peuvent recevoir une valeur par défaut, pourquoi ne pouvons-nous pas faire de même pour les variables locales?

Et cela conduit également à des problèmes comme expliqué dans ce commentaire à un article de blog :

Eh bien, cette règle est la plus frustrante lorsque vous essayez de fermer une ressource dans un bloc final. Si j'instancie la ressource à l'intérieur d'un essai, mais que j'essaie de la fermer dans le enfin, j'obtiens cette erreur. Si je déplace l'instanciation en dehors de l'essai, j'obtiens une autre erreur indiquant qu'il doit être dans un essai.

Très frustrant.

Shivasubramanian A
la source
1
Désolé pour ça ... cette question n'a pas surgi quand je la tapais ... cependant, je suppose qu'il y a une différence entre les deux questions ... Je veux savoir pourquoi les concepteurs de Java l'ont conçu de cette façon, alors que la question que vous avez pointée ne pose pas cela ...
Shivasubramanian A
Voir aussi cette question C # connexe: stackoverflow.com/questions/1542824/…
Raedwald
Simplement - parce qu'il est facile pour le compilateur de suivre les variables locales non initialisées. Si cela pouvait faire la même chose avec d'autres variables, ce serait le cas. Le compilateur essaie simplement de vous aider.
rustyx

Réponses:

62

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

guerrier
la source
23

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

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

La plainte du commentateur est que le compilateur rechigne à la ligne dans la finallysection, affirmant que cela sopourrait être non 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 dans un essai». Je suppose que cela signifie qu'une partie du code peut soulever 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'exceptions, donc tout ce qui est lié aux exceptions dans la première version devrait fonctionner de la même manière dans la seconde.

Quoi qu'il en soit, cette deuxième version du code est la bonne façon de l'écrire. Dans la première version, le message d'erreur du compilateur était correct. La sovariable n'est peut-être pas initialisée. En particulier, si le SomeObjectconstructeur échoue, soil ne sera pas initialisé, et donc ce sera une erreur de tenter d'appeler so.CleanUp. Entrez toujours dans la trysection après avoir acquis la ressource que la finallysection finalise.

Le bloc try- finallyaprès l' soinitialisation n'est là que pour protéger l' SomeObjectinstance, pour s'assurer qu'elle est nettoyée quoi qu'il arrive. S'il y a d' autres choses qui ont besoin de courir, mais ils ne sont pas liés si l' SomeObjectinstance a été attribué la propriété, alors ils devraient aller dans un autre try - finallybloc, probablement celui qui enveloppe celui que je l' ai montré.

Le fait d'exiger que les variables soient affectées manuellement avant leur utilisation ne pose pas de réels problèmes. Cela ne conduit qu'à des tracas mineurs, mais votre code sera meilleur pour cela. Vous aurez des variables ayant une portée plus limitée, et try- les finallyblocs qui ne cherchent pas à protéger trop.

Si les variables locales avaient des valeurs par défaut, alors sodans le premier exemple aurait été null. Cela n'aurait vraiment rien résolu. Au lieu d'obtenir une erreur de compilation dans le finallybloc, vous auriez une erreur NullPointerExceptioncachée là-bas qui pourrait cacher toute autre exception qui pourrait se produire dans la section «Faites un peu de travail ici» du code. (Ou est-ce que les exceptions dans les finallysections s'enchaînent automatiquement à l'exception précédente? Je ne me souviens pas. Même dans ce cas, vous auriez une exception supplémentaire par rapport à la vraie.)

Rob Kennedy
la source
2
pourquoi ne pas avoir simplement un if (so! = null) ... dans le bloc finally?
izb
cela causera toujours l'avertissement / l'erreur du compilateur - je ne pense pas que le compilateur comprend que si c'est vérifié (mais je fais juste cela hors de la mémoire, non testé).
Chii
6
Je mettrais juste SomeObject so = null avant d'essayer et mettre la vérification null à la clause finally. De cette façon, il n'y aura aucun avertissement du compilateur.
Juha Syrjälä le
Pourquoi compliquer les choses? Écrivez le bloc try-finally de cette façon, et vous SAVEZ que la variable a une valeur valide. Aucune vérification nulle requise.
Rob Kennedy
1
Rob, votre exemple "new SomeObject ()" est simple et aucune exception ne doit y être générée, mais si l'appel peut générer des exceptions, il serait préférable de le faire apparaître dans le bloc try afin qu'il puisse être traité.
Sarel Botha
12

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èvera une NullPointerException

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

Ce que j'ai tendance à faire, c'est ceci:

SomeObject so = null;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  if (so != null) {
     so.CleanUp(); // safe
  }
}
Moine électrique
la source
12
Que préfèrerais-tu faire?
Electric Monk
2
Ouais, moche. Ouais, c'est ce que je fais aussi.
SMBiggs
@ElectricMonk Quelle forme vous pensez être la meilleure, celle que vous avez montrée ou celle montrée ici dans la méthode getContents (..): javapractices.com/topic/TopicAction.do?Id=126
Atom
11

Notez que l'instance finale / les variables membres ne sont pas initialisées par défaut. Parce que ceux-ci sont définitifs et ne peuvent pas ê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 force le programmeur à l'initialiser.

En revanche, les variables membres non finales 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 plus tard. En ce qui concerne les variables locales, la portée des variables locales est beaucoup plus étroite. Le compilateur sait quand il est utilisé. Par conséquent, forcer le programmeur à initialiser la variable a du sens.

Adeel Ansari
la source
9

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

Pourquoi ne pas faire un pas supplémentaire? Prenez du recul - 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 à null lors de la première passe (lorsque vous la codez pour la première fois). Affectez-le à la valeur réelle ou ne l'attribuez pas du tout, car si vous ne le faites pas, java peut vous dire quand vous vous trompez vraiment. Prenons la réponse d'Electric Monk comme un excellent exemple. Dans le premier cas, il est en fait incroyablement utile de vous dire que si le try () échoue parce que le constructeur de SomeObject a lancé une exception, vous vous retrouverez avec un NPE dans le fichier. Si le constructeur ne peut pas lever d'exception, il ne devrait pas être dans l'essai.

Cet avertissement est un formidable vérificateur de mauvais programmeur multi-chemins qui m'a sauvé de faire des choses stupides car il vérifie chaque chemin et s'assure que si vous avez utilisé la variable dans un chemin, vous deviez l'initialiser dans chaque chemin qui y mène . Je n'initialise jamais explicitement les variables jusqu'à ce que je détermine que c'est la bonne chose à faire.

En plus de cela, n'est-il pas préférable de dire explicitement "int size = 0" plutôt que "int size" et faire comprendre au prochain programmeur que vous avez l'intention 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.

Bill K
la source
1
Oui, et il y en a d'autres où il doit plus ou moins être initialisé à null à cause de la façon dont le code s'écoule - je n'aurais pas dû dire "jamais" mis à jour la réponse pour refléter cela.
Bill K
4

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 qui réduiront le problème à un point minimal. 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.

Mehrdad Afshari
la source
1
On peut soutenir que le compilateur pourrait déterminer si vous affectez toujours à la variable avant de faire quoi que ce soit avec elle, et supprimer une affectation de valeur par défaut automatique dans un tel cas. Si le compilateur ne peut pas déterminer si une affectation se produit avant l'accès, il génère l'affectation par défaut.
Greg Hewgill
2
Oui, mais on pourrait dire que cela permet au programmeur de savoir s'il a laissé la variable non initialisée par erreur.
Mehrdad Afshari
1
Le compilateur peut également le faire dans les deux cas. :) Personnellement, je préférerais que le compilateur traite une variable non initialisée comme une erreur. Cela signifie que j'ai peut-être commis une erreur quelque part.
Greg Hewgill
Je ne suis pas un gars de Java, mais j'aime la manière C # de le gérer. La différence est dans ce cas, le compilateur a dû émettre un avertissement, ce qui pourrait vous faire obtenir quelques centaines d'avertissements pour votre programme correct;)
Mehrdad Afshari
Avertit-il également les variables membres?
Adeel Ansari
4

(Il peut sembler étrange de publier une nouvelle réponse si longtemps après la question, mais un doublon 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 affecter de valeur, c'est presque certainement une erreur de logique.

Cela dit, je pourrais tout à fait 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., pris au début des années 90, alors nous y sommes. (Et je ne dis pas qu'ils ont fait le mauvais appel.)

je pouvais Cependant, pas me mettre à l'écart des variables locales par défaut. Oui, nous ne devrions pas compter sur des compilateurs pour vérifier notre logique, et ce n'est pas le cas, mais c'est toujours pratique lorsque le compilateur en détecte un. :-)

TJ Crowder
la source
"Cela dit, je pourrais tout à fait me retenir d'exiger que les variables d'instance soient toujours explicitement initialisées ..." ce qui, FWIW, est la direction qu'elles ont prise dans TypeScript.
TJ Crowder le
3

Il est plus efficace de ne pas initialiser les variables, et dans le cas des 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'initialiser une variable, vous pouvez toujours le faire vous-même, ce n'est donc pas un problème.

starblue
la source
3

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 devrait y avoir peu de raisons d'incertitude quant à la valeur, ou du moins, à l'origine de cette valeur. Je pourrais imaginer de nombreuses erreurs résultant du fait d'avoir 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 variables locales se voient attribuer 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);

En fin de compte, en supposant que le compilateur attribue une valeur par défaut de '\ 0' à letterGrade , ce code tel qu'il est écrit fonctionnerait correctement. Cependant, que se passe-t-il si nous oublions la déclaration else?

Un test de notre code peut entraîner les résultats suivants

Enter grade
43
Your grade is

Ce résultat, bien que prévisible, n'était certainement pas l'intention du codeur. En effet, probablement dans la grande 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 , donc dans la grande majorité des cas, la valeur par défaut entraînerait une erreur. Il est plus logique 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 = 1in l' for(int i = 1; i < 10; i++)emporte de loin sur la commodité de ne pas avoir à inclure le = 0infor(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 en fait un catch-22 comme la citation semble le suggérer), quand par exemple un objet lève une exception vérifiée dans son constructeur, mais pour une raison ou autre, quelque chose doit être fait à cet objet à la fin du bloc en enfin. Un exemple parfait de cela est lorsqu'il s'agit de 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 finally n'est plus nécessaire en utilisant 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égoûtant, 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 la façon 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. Quand 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 à la place la valeur initiale est n'importe quelle série de 1 et de 0 se trouvant 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 cela Same.puisse être affiché dans une course et Not same.pourrait être affiché dans un autre. Le problème peut devenir encore plus grave une fois que vous commencez à 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 nul). Cependant, si la valeur initiale est n'importe quelle série de 0 et de 1 se produisant à cet emplacement de mémoire, non seulement il n'y a aucune garantie que vous obtiendrez les mêmes résultats à chaque fois, mais il n'y a pas non plus de garantie que l'objet s [0] pointe to (en supposant que cela indique quelque chose de significatif) est même une chaîne (peut-être que c'est un lapin,: p )! Ce manque de souci du type irait à l'encontre de pratiquement tout ce qui fait de Java Java. Ainsi, bien que les valeurs par défaut des variables locales puissent être considérées au mieux comme facultatives, avoir des valeurs par défaut pour les variables d'instance est plus proche d'une nécessité .

kumquatfelafel
la source
1

si je ne me trompe pas, une autre raison pourrait être

Donner la valeur par défaut des variables membres fait partie du chargement de classe

Le chargement de classe est une chose au moment de l'exécution en java signifie que lorsque vous créez un objet, la classe est chargée avec le chargement de classe seules les variables membres sont initialisées avec la valeur par défaut JVM ne prend pas le temps de donner une valeur par défaut à vos variables locales car certaines méthodes ne le seront jamais appelé parce que l'appel de méthode peut être conditionnel, alors pourquoi prendre le temps de leur donner la valeur par défaut et réduire les performances si ces valeurs par défaut ne seront jamais utilisées.

Abhinav Chauhan
la source
0

Eclipse vous donne même des avertissements de variables non initialisées, donc cela devient tout à fait évident de toute façon. Personnellement, je pense que c'est une bonne chose que ce soit le comportement par défaut, sinon votre application peut utiliser des valeurs inattendues, et au lieu que le compilateur lève une erreur, il ne fera rien (mais peut-être donner un avertissement) et vous gratterez votre tête à savoir pourquoi certaines choses ne se comportent pas tout à fait comme elles le devraient.

Kezzer
la source
0

Les variables locales sont stockées sur une pile, mais les variables d'instance sont stockées sur le tas, il y a donc quelques chances qu'une valeur précédente sur la pile soit lue au lieu d'une valeur par défaut comme cela se produit dans le tas. Pour cette raison, le jvm ne permet pas d'utiliser une variable locale sans l'initialiser.

David Santamaria
la source
2
Complètement faux ... toutes les non-primitives Java sont stockées dans le tas indépendamment du moment et de la façon dont elles sont construites
gshauger
Avant Java 7, les variables d'instance sont stockées sur le tas et les variables locales se trouvent sur la pile. Cependant, tout objet référencé par une variable locale sera trouvé dans le tas. À partir de Java 7, le "Java Hotspot Server Compiler" peut effectuer une "analyse d'échappement" et décider d'allouer certains objets sur la pile au lieu du tas.
mamills
0

La variable d'instance aura des valeurs par défaut mais les variables locales ne peuvent pas avoir de valeurs par défaut. Puisque 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 chronophage de vérifier les raisons des réponses inattendues.

Xiaogang
la source
-1

La réponse est que les variables d'instance peuvent être initialisées dans le constructeur de classe ou dans n'importe quelle méthode de classe, mais dans le cas de variables locales, une fois que vous avez défini tout ce qui dans la méthode reste à jamais dans la classe.

Akash5288
la source
-2

Je pourrais penser à 2 raisons suivantes

  1. Comme la plupart des réponses l'ont dit en mettant la contrainte d'initialisation de la variable locale, on s'assure que la variable locale reçoit une valeur comme le veut le programmeur et s'assure que les résultats attendus sont calculés.
  2. Les variables d'instance peuvent être masquées en déclarant des variables locales (même nom) - pour garantir le comportement attendu, les variables locales sont forcées d'être initialisées à une valeur. (J'éviterais totalement cela cependant)
Mitra
la source
Les champs ne peuvent pas être remplacés. Tout au plus peuvent-ils être cachés et je ne vois pas comment le masquage interférerait avec une vérification d'initialisation?
meriton
Caché à droite.Si on décide de créer une variable locale avec le même nom que l'instance, à cause de cette contrainte, la variable locale sera initialisée avec une valeur éventuellement sélectionnée (autre que la valeur de la variable d'instance)
Mitra