Pourquoi ce code Java se compile-t-il?

96

Dans la portée de la méthode ou de la classe, la ligne ci-dessous compile (avec avertissement):

int x = x = 1;

Dans la portée de classe, où les variables obtiennent leurs valeurs par défaut , ce qui suit donne l'erreur `` référence indéfinie '':

int x = x + 1;

N'est-ce pas le premier qui x = x = 1devrait se retrouver avec la même erreur de «référence indéfinie»? Ou peut-être que la deuxième ligne int x = x + 1devrait être compilée? Ou il me manque quelque chose?

Marcin
la source
1
Si vous ajoutez le mot-clé staticdans la variable class-scope, comme dans static int x = x + 1;, obtiendrez-vous la même erreur? Parce qu'en C #, cela fait une différence si c'est statique ou non statique.
Jeppe Stig Nielsen
static int x = x + 1échoue en Java.
Marcin
1
dans c # les deux int a = this.a + 1;et int b = 1; int a = b + 1;dans la portée de la classe (qui sont tous deux ok en Java) échouent, probablement en raison du §17.4.5.2 - "Un initialiseur de variable pour un champ d'instance ne peut pas référencer l'instance en cours de création." Je ne sais pas si c'est explicitement autorisé quelque part mais statique n'a pas une telle restriction. En Java , les règles sont différentes et static int x = x + 1ne parvient pas pour la même raison que le int x = x + 1fait
MSAM
Cette réponse avec un bytecode efface tous les doutes.
rgripper

Réponses:

101

tl; dr

Pour les champs , int b = b + 1est illégal car il bs'agit d'une référence directe illégale à b. Vous pouvez réellement résoudre ce problème en écrivant int b = this.b + 1, qui compile sans se plaindre.

Pour les variables locales , int d = d + 1est illégal car dn'est pas initialisé avant utilisation. Ce n'est pas le cas pour les champs, qui sont toujours initialisés par défaut.

Vous pouvez voir la différence en essayant de compiler

int x = (x = 1) + x;

comme déclaration de champ et comme déclaration de variable locale. Le premier échouera, mais le second réussira, en raison de la différence de sémantique.

introduction

Tout d'abord, les règles des initialiseurs de champ et de variable locale sont très différentes. Cette réponse abordera donc les règles en deux parties.

Nous utiliserons ce programme de test partout:

public class test {
    int a = a = 1;
    int b = b + 1;
    public static void Main(String[] args) {
        int c = c = 1;
        int d = d + 1;
    }
}

La déclaration de bn'est pas valide et échoue avec une illegal forward referenceerreur.
La déclaration de dn'est pas valide et échoue avec une variable d might not have been initializederreur.

Le fait que ces erreurs soient différentes devrait indiquer que les raisons des erreurs sont également différentes.

Des champs

Les initialiseurs de champs en Java sont régis par JLS §8.3.2 , Initialisation des champs.

La portée d'un champ est définie dans JLS §6.3 , Portée d'une déclaration.

Les règles pertinentes sont:

  • La portée d'une déclaration d'un membre mdéclaré dans ou héritée par une classe de type C (§8.1.6) est le corps entier de C, y compris toutes les déclarations de type imbriquées.
  • Les expressions d'initialisation pour les variables d'instance peuvent utiliser le nom simple de toute variable statique déclarée ou héritée par la classe, même celle dont la déclaration se produit textuellement plus tard.
  • L'utilisation de variables d'instance dont les déclarations apparaissent textuellement après l'utilisation est parfois restreinte, même si ces variables d'instance sont dans la portée. Voir §8.3.2.3 pour les règles précises régissant la référence directe aux variables d'instance.

§8.3.2.3 dit:

La déclaration d'un membre doit apparaître textuellement avant d'être utilisée uniquement si le membre est un champ d'instance (respectivement statique) d'une classe ou d'une interface C et que toutes les conditions suivantes sont remplies:

  • L'utilisation se produit dans un initialiseur de variable d'instance (respectivement statique) de C ou dans un initialiseur d'instance (respectivement statique) de C.
  • L'utilisation n'est pas sur le côté gauche d'une affectation.
  • L'utilisation se fait via un simple nom.
  • C est la classe ou l'interface la plus interne englobant l'utilisation.

Vous pouvez en fait faire référence à des champs avant qu'ils n'aient été déclarés, sauf dans certains cas. Ces restrictions visent à empêcher le code comme

int j = i;
int i = j;

de la compilation. La spécification Java indique que "les restrictions ci-dessus sont conçues pour intercepter, au moment de la compilation, les initialisations circulaires ou autrement mal formées".

À quoi se résument ces règles?

En bref, les règles disent essentiellement que vous devez déclarer un champ avant une référence à ce champ si (a) la référence est dans un initialiseur, (b) la référence n'est pas affectée à, (c) la référence est un nom simple (pas de qualificatif comme this.) et (d) il n'est pas accessible depuis une classe interne. Ainsi, une référence directe qui satisfait les quatre conditions est illégale, mais une référence directe qui échoue sur au moins une condition est OK.

int a = a = 1;compile parce qu'il viole (b): la référence a est assignée, il est donc légal de s'y référer aavant ala déclaration complète de.

int b = this.b + 1compile également car il viole (c): la référence this.bn'est pas un simple nom (il est qualifié par this.). Cette construction étrange est toujours parfaitement bien définie, car elle this.ba la valeur zéro.

Donc, fondamentalement, les restrictions sur les références de champ dans les initialiseurs empêchent int a = a + 1une compilation réussie.

Notez que la déclaration sur le terrain int b = (b = 1) + bva échouer à compiler, parce que la finale best encore une référence avant illégale.

Variables locales

Les déclarations de variables locales sont régies par JLS §14.4 , Instructions de déclaration de variables locales.

La portée d'une variable locale est définie dans JLS §6.3 , Portée d'une déclaration:

  • La portée d'une déclaration de variable locale dans un bloc (§14.4) est le reste du bloc dans lequel la déclaration apparaît, en commençant par son propre initialiseur et en incluant tout autre déclarateur à droite dans l'instruction de déclaration de variable locale.

Notez que les initialiseurs sont dans la portée de la variable déclarée. Alors pourquoi ne int d = d + 1;compile pas ?

La raison est due à la règle de Java sur l'affectation définie ( JLS §16 ). L'affectation définie indique essentiellement que chaque accès à une variable locale doit avoir une affectation précédente à cette variable, et le compilateur Java vérifie les boucles et les branches pour s'assurer que l'affectation se produit toujours avant toute utilisation (c'est pourquoi une affectation définie a une section de spécification entière dédiée à lui). La règle de base est:

  • Pour chaque accès à une variable locale ou à un champ final vide x, xdoit être définitivement attribué avant l'accès, ou une erreur de compilation se produit.

Dans int d = d + 1;, l'accès à dest résolu à la variable locale bien, mais comme dn'a pas été affecté avant l' daccès, le compilateur émet une erreur. Dans int c = c = 1, c = 1se produit en premier, qui affecte c, puis cest initialisé au résultat de cette affectation (qui est 1).

Notez qu'en raison de règles d'affectation définies, la déclaration de variable locale int d = (d = 1) + d; sera compilée avec succès ( contrairement à la déclaration de champ int b = (b = 1) + b), car elle dest définitivement affectée au moment où la finale dest atteinte.

nneonneo
la source
+1 pour les références, mais je pense que vous vous êtes trompé de formulation: "int a = a = 1; compile parce qu'il viole (b)", s'il violait l'une des 4 exigences, il ne compilerait pas. Cependant , il ne car il EST sur le côté gauche d'une cession (double négation dans le libellé de JLS ne contribue pas beaucoup ici). En int b = b + 1b se trouve à droite (pas à gauche) de l'affectation, donc cela violerait cela ...
msam
... Ce dont je ne suis pas trop sûr, c'est la suivante: ces 4 conditions doivent être remplies si la déclaration n'apparaît pas textuellement avant la cession, dans ce cas, je pense que la déclaration apparaît "textuellement" avant la cession int x = x = 1, dans laquelle au cas où rien de tout cela ne s'appliquerait.
msam
@msam: C'est un peu déroutant, mais en gros, vous devez violer l'une des quatre conditions pour faire une référence avant. Si votre référence directe remplit les quatre conditions, elle est illégale.
nneonneo
@msam: De plus, la déclaration complète ne prend effet qu'après l'initialisation.
nneonneo
@mrfishie: Grande réponse, mais il y a une profondeur surprenante dans la spécification Java. La question n'est pas aussi simple qu'il y paraît à première vue. (J'ai écrit un sous-ensemble de compilateur Java une fois, donc je suis assez familier avec la plupart des tenants et aboutissants du JLS).
nneonneo
86
int x = x = 1;

est équivalent à

int x = 1;
x = x; //warning here

pendant que dans

int x = x + 1; 

nous devons d'abord calculer x+1mais la valeur de x n'est pas connue donc vous obtenez une erreur (le compilateur sait que la valeur de x n'est pas connue)

msam
la source
4
Ceci plus l'indication sur la bonne associativité d'OpenSauce que j'ai trouvé très utile.
TobiMcNamobi
1
Je pensais que la valeur de retour d'une affectation était la valeur attribuée, pas la valeur de la variable.
zzzzBov
2
@zzzzBov a raison. int x = x = 1;équivaut à int x = (x = 1), non x = 1; x = x; . Vous ne devriez pas recevoir d'avertissement du compilateur pour cela.
nneonneo
int x = x = 1;s équivalent à int en x = (x = 1)raison de l'associativité à droite de l' =opérateur
Grijesh Chauhan
1
@nneonneo et int x = (x = 1)équivaut à int x; x = 1; x = x;(déclaration de variable, évaluation de l'initialiseur de champ, affectation de variable au résultat de ladite évaluation), d'où l'avertissement
msam
41

C'est à peu près équivalent à:

int x;
x = 1;
x = 1;

Premièrement, int <var> = <expression>;équivaut toujours à

int <var>;
<var> = <expression>;

Dans ce cas, votre expression est x = 1, qui est également une déclaration. x = 1est une instruction valide, puisque le var xa déjà été déclaré. C'est aussi une expression avec la valeur 1, qui est ensuite affectée à xnouveau.

OpenSauce
la source
Ok, mais si ça se passe comme tu le dis, pourquoi dans la portée de la classe la deuxième instruction donne une erreur? Je veux dire que vous obtenez la 0valeur par défaut pour ints, donc je m'attendrais à ce que le résultat soit 1, pas le undefined reference.
Marcin
Jetez un œil à la réponse @izogfif. Cela semble fonctionner, car le compilateur C ++ attribue des valeurs par défaut aux variables. De la même manière que java le fait pour les variables de niveau classe.
Marcin
@Marcin: en Java, les entiers ne sont pas initialisés à 0 lorsqu'ils sont des variables locales. Ils ne sont initialisés à 0 que s'ils sont des variables membres. Donc, dans votre deuxième ligne, x + 1n'a pas de valeur définie, car il xn'est pas initialisé.
OpenSauce
1
@OpenSauce But x est défini comme une variable membre ("dans la portée de la classe").
Jacob Raihle
@JacobRaihle: Ah ok, je n'ai pas repéré cette partie. Je ne suis pas sûr que le bytecode pour initialiser un var à 0 sera généré par le compilateur s'il voit qu'il existe une instruction d'initialisation explicite. Il y a un article ici qui donne des détails sur l'initialisation des classes et des objets, bien que je ne pense pas qu'il résout exactement ce problème: javaworld.com/jw-11-2001/jw-1102-java101.html
OpenSauce
12

En java ou dans n'importe quelle langue moderne, l'affectation vient de la droite.

Supposons que vous ayez deux variables x et y,

int z = x = y = 5;

Cette déclaration est valide et c'est ainsi que le compilateur les divise.

y = 5;
x = y;
z = x; // which will be 5

Mais dans ton cas

int x = x + 1;

Le compilateur a donné une exception car, il se divise comme ceci.

x = 1; // oops, it isn't declared because assignment comes from the right.
Sri Harsha Chilakapati
la source
l'avertissement est activé x = x pas x = 1
Asim Ghaffar
8

int x = x = 1; n'est pas égal à:

int x;
x = 1;
x = x;

javap nous aide à nouveau, ce sont des instructions JVM générées pour ce code:

0: iconst_1    //load constant to stack
1: dup         //duplicate it
2: istore_1    //set x to constant
3: istore_1    //set x to constant

plus comme:

int x = 1;
x = 1;

Il n'y a aucune raison de lancer une erreur de référence non définie. Il y a maintenant une utilisation de variable avant son initialisation, donc ce code est entièrement conforme aux spécifications. En fait, il n'y a pas d'utilisation du tout de variable , juste des affectations. Et le compilateur JIT ira encore plus loin, il éliminera de telles constructions. Pour dire honnêtement, je ne comprends pas comment ce code est connecté à la spécification JLS d'initialisation et d'utilisation des variables. Aucune utilisation, aucun problème. ;)

Veuillez corriger si je me trompe. Je ne comprends pas pourquoi les autres réponses, qui font référence à de nombreux paragraphes JLS, rassemblent autant d'avantages. Ces paragraphes n'ont rien de commun avec ce cas. Juste deux affectations en série et rien de plus.

Si nous écrivons:

int b, c, d, e, f;
int a = b = c = d = e = f = 5;

est égal à:

f = 5
e = 5
d = 5
c = 5
b = 5
a = 5

L'expression la plus à droite est simplement affectée aux variables une par une, sans aucune récursivité. Nous pouvons désordre les variables comme nous le souhaitons:

a = b = c = f = e = d = a = a = a = a = a = e = f = 5;
Mikhail
la source
7

Dans int x = x + 1;vous ajoutez 1 à x, donc quelle est la valeur de x, il n'est pas encore créé.

Mais in int x=x=1;compilera sans erreur car vous attribuez 1 à x.

Alya'a Gamal
la source
5

Votre premier morceau de code contient un deuxième =au lieu d'un plus. Cela compilera n'importe où tandis que le deuxième morceau de code ne se compilera à aucun endroit.

Joe Elleson
la source
5

Dans le deuxième morceau de code, x est utilisé avant sa déclaration, tandis que dans le premier morceau de code, il est simplement attribué deux fois, ce qui n'a pas de sens mais est valide.

WilQu
la source
5

Décomposons-le étape par étape, à droite associative

int x = x = 1

x = 1, affectez 1 à une variable x

int x = x, assignez ce que x est à lui-même, comme un int. Étant donné que x était précédemment attribué à 1, il conserve 1, bien que de manière redondante.

Cela compile bien.

int x = x + 1

x + 1, ajoutez un à une variable x. Cependant, x étant non défini, cela provoquera une erreur de compilation.

int x = x + 1, ainsi cette ligne compile les erreurs car la partie droite des égaux ne compilera pas en ajoutant une à une variable non attribuée

steventnorris
la source
Non, c'est associatif à droite lorsqu'il y a deux =opérateurs, donc c'est la même chose que int x = (x = 1);.
Jeppe Stig Nielsen
Ah, mes commandes sont arrêtées. Désolé pour ça. J'aurais dû les faire à l'envers. Je l'ai changé maintenant.
steventnorris
3

Le second int x=x=1est compilé car vous affectez la valeur au x mais dans les autres cas int x=x+1ici la variable x n'est pas initialisée, rappelez-vous en java les variables locales ne sont pas initialisées à la valeur par défaut. Remarque Si c'est ( int x=x+1) dans la portée de la classe également, cela donnera également une erreur de compilation car la variable n'est pas créée.

Krushna
la source
2
int x = x + 1;

compile avec succès dans Visual Studio 2008 avec avertissement

warning C4700: uninitialized local variable 'x' used`
izogfif
la source
2
Intéressant. Est-ce C / C ++?
Marcin
@Marcin: oui, c'est C ++. @msam: désolé, je pense que j'ai vu tag cau lieu de javamais apparemment c'était l'autre question.
izogfif
Il compile car en C ++, les compilateurs attribuent des valeurs par défaut aux types primitifs. Utilisez bool y;et y==trueretournera false.
Sri Harsha Chilakapati
@SriHarshaChilakapati, est-ce une sorte de standard dans le compilateur C ++? Parce que lorsque je compile void main() { int x = x + 1; printf("%d ", x); }dans Visual Studio 2008, dans Debug, j'obtiens l'exception Run-Time Check Failure #3 - The variable 'x' is being used without being initialized.et dans Release, le numéro est 1896199921imprimé dans la console.
izogfif
1
@SriHarshaChilakapati Parlant d'autres langages: En C #, pour un staticchamp (variable statique au niveau de la classe), les mêmes règles s'appliquent. Par exemple, un champ déclaré comme public static int x = x + 1;compile sans avertissement dans Visual C #. Peut-être la même chose en Java?
Jeppe Stig Nielsen
2

x est non initialisé dans x = x + 1;.

Le langage de programmation Java est de type statique, ce qui signifie que toutes les variables doivent d'abord être déclarées avant de pouvoir être utilisées.

Voir les types de données primitifs

Mohan Raj B
la source
3
La nécessité d'initialiser les variables avant d'utiliser leurs valeurs n'a rien à voir avec le typage statique. Typiquement statique: vous devez déclarer le type d'une variable. Initialiser avant utilisation: il doit avoir une valeur prouvée avant de pouvoir l'utiliser.
Jon Bright
@JonBright: La nécessité de déclarer des types de variables n'a rien à voir avec le typage statique. Par exemple, il existe des langages typés statiquement avec inférence de type.
hammar
@hammar, à mon avis, vous pouvez l'argumenter de deux manières: avec l'inférence de type, vous déclarez implicitement le type de la variable d'une manière que le système peut déduire. Ou bien, l'inférence de type est une troisième méthode, où les variables ne sont pas typées dynamiquement à l'exécution, mais sont au niveau de la source, en fonction de leur utilisation et des inférences ainsi faites. Quoi qu'il en soit, la déclaration reste vraie. Mais vous avez raison, je ne pensais pas à d'autres systèmes de types.
Jon Bright
2

La ligne de code ne se compile pas avec un avertissement en raison de la façon dont le code fonctionne réellement. Lorsque vous exécutez le code int x = x = 1, Java crée d'abord la variable x, comme défini. Ensuite, il exécute le code d'affectation ( x = 1). Comme xest déjà défini, le système n'a pas d'erreurs en définissant xsur 1. Cela renvoie la valeur 1, car c'est maintenant la valeur de x. Par conséquent, xest maintenant finalement défini sur 1.
Java exécute le code comme s'il s'agissait de ceci:

int x;
x = (x = 1); // (x = 1) returns 1 so there is no error

Cependant, dans votre deuxième morceau de code ,, int x = x + 1l' + 1instruction doit xêtre définie, ce qui n'est plus le cas. Étant donné que les instructions d'affectation signifient toujours que le code à droite de =est exécuté en premier, le code échouera car il xn'est pas défini. Java exécuterait le code comme ceci:

int x;
x = x + 1; // this line causes the error because `x` is undefined
tombeau
la source
-1

Complier a lu les déclarations de droite à gauche et nous avons conçu pour faire le contraire. C'est pourquoi cela a agacé au début. Faites-en une habitude pour lire les instructions (code) de droite à gauche, vous n'aurez pas un tel problème.

Ramiz Uddin
la source