Quelles sont les règles pour l'ordre d'évaluation en Java?

86

Je lis du texte Java et j'ai le code suivant:

int[] a = {4,4};
int b = 1;
a[b] = b = 0;

Dans le texte, l'auteur n'a pas donné d'explication claire et l'effet de la dernière ligne est: a[1] = 0;

Je ne suis pas sûr de comprendre: comment l'évaluation s'est-elle déroulée?

ipkiss
la source
19
La confusion ci-dessous sur la raison pour laquelle cela se produit signifie d'ailleurs que vous ne devriez jamais faire cela, car beaucoup de gens devront réfléchir à ce que cela fait réellement, au lieu que cela soit évident.
Martijn le
La bonne réponse à toute question comme celle-ci est "ne faites pas ça!" L'affectation doit être traitée comme une déclaration; l'utilisation de l'affectation comme expression qui renvoie une valeur devrait déclencher une erreur du compilateur, ou à tout le moins un avertissement. Ne fais pas ça.
Mason Wheeler

Réponses:

173

Permettez-moi de le dire très clairement, car les gens comprennent mal cela tout le temps:

L'ordre d'évaluation des sous-expressions est indépendant à la fois de l'associativité et de la préséance . L'associativité et la priorité déterminent dans quel ordre les opérateurs sont exécutés mais ne déterminent pas dans quel ordre les sous - expressions sont évaluées. Votre question porte sur l'ordre dans lequel les sous - expressions sont évaluées.

Considérez A() + B() + C() * D(). La multiplication a une priorité plus élevée que l'addition, et l'addition est associative à gauche, donc cela équivaut à (A() + B()) + (C() * D()) Mais sachant que cela vous indique seulement que la première addition se produira avant la deuxième addition, et que la multiplication se produira avant la deuxième addition. Il ne vous dit pas dans quel ordre A (), B (), C () et D () seront appelés! (Il ne vous dit pas non plus si la multiplication se produit avant ou après la première addition.) Il serait parfaitement possible d'obéir aux règles de préséance et d'associativité en compilant ceci comme:

d = D()          // these four computations can happen in any order
b = B()
c = C()
a = A()
sum = a + b      // these two computations can happen in any order
product = c * d
result = sum + product // this has to happen last

Toutes les règles de préséance et d'associativité y sont suivies - la première addition se produit avant la deuxième addition, et la multiplication se produit avant la deuxième addition. Clairement, nous pouvons faire les appels à A (), B (), C () et D () dans n'importe quel ordre tout en obéissant aux règles de préséance et d'associativité!

Nous avons besoin d'une règle sans rapport avec les règles de préséance et d'associativité pour expliquer l'ordre dans lequel les sous-expressions sont évaluées. La règle pertinente en Java (et C #) est "les sous-expressions sont évaluées de gauche à droite". Puisque A () apparaît à gauche de C (), A () est évalué en premier, indépendamment du fait que C () est impliqué dans une multiplication et A () n'est impliqué que dans une addition.

Alors maintenant, vous avez suffisamment d'informations pour répondre à votre question. Dans a[b] = b = 0les règles d'associativité disons que c'est, a[b] = (b = 0);mais cela ne veut pas dire que les b=0courses en premier! Les règles de priorité indiquent que l'indexation a une priorité plus élevée que l'affectation, mais cela ne signifie pas que l'indexeur s'exécute avant l'affectation la plus à droite .

(MISE À JOUR: Une version précédente de cette réponse avait quelques petites omissions pratiquement sans importance dans la section qui suit que j'ai corrigée. J'ai également écrit un article de blog décrivant pourquoi ces règles sont sensées en Java et C # ici: https: // ericlippert.com/2019/01/18/indexer-error-cases/ )

La préséance et l'associativité nous indiquent seulement que l'affectation de zéro à bdoit se produire avant l'affectation à a[b], car l'affectation de zéro calcule la valeur qui est affectée dans l'opération d'indexation. La préséance et l'associativité seules ne disent rien sur le fait de savoir si le a[b]est évalué avant ou après le b=0.

Encore une fois, c'est exactement la même chose que: A()[B()] = C()- Tout ce que nous savons, c'est que l'indexation doit avoir lieu avant l'affectation. Nous ne savons pas si A (), B () ou C () s'exécute en premier en fonction de la priorité et de l'associativité . Nous avons besoin d'une autre règle pour nous le dire.

La règle est, encore une fois, "lorsque vous avez le choix de ce que vous devez faire en premier, allez toujours de gauche à droite". Cependant, il y a une ride intéressante dans ce scénario spécifique. L'effet secondaire d'une exception levée est-il causé par une collection nulle ou un index hors plage considéré comme faisant partie du calcul du côté gauche de l'affectation, ou une partie du calcul de l'affectation elle-même? Java choisit ce dernier. (Bien sûr, c'est une distinction qui n'a d'importance que si le code est déjà erroné , car le code correct ne déréférencera pas null ou ne passera pas un mauvais index en premier lieu.)

Alors que se passe-t-il?

  • Le a[b]est à gauche du b=0, donc les a[b]exécutions en premier , résultant en a[1]. Cependant, la vérification de la validité de cette opération d'indexation est retardée.
  • Puis le b=0se produit.
  • Ensuite, la vérification qui aest valide et a[1]est à portée se produit
  • L'affectation de la valeur se a[1]produit en dernier.

Donc, bien que dans ce cas spécifique il y ait quelques subtilités à considérer pour ces rares cas d'erreur qui ne devraient pas se produire dans un code correct en premier lieu, en général, vous pouvez raisonner: les choses à gauche se produisent avant les choses à droite . C'est la règle que vous recherchez. Parler de priorité et d'associativité est à la fois déroutant et hors de propos.

Les gens se trompent tout le temps , même ceux qui devraient être mieux informés. J'ai édité beaucoup trop de livres de programmation qui énonçaient les règles de manière incorrecte, il n'est donc pas surprenant que beaucoup de gens aient des croyances complètement incorrectes sur la relation entre la préséance / associativité et l'ordre d'évaluation - à savoir qu'en réalité, il n'y a pas de telle relation. ; ils sont indépendants.

Si ce sujet vous intéresse, consultez mes articles sur le sujet pour en savoir plus:

http://blogs.msdn.com/b/ericlippert/archive/tags/precedence/

Ils concernent C #, mais la plupart de ces éléments s'appliquent également à Java.

Eric Lippert
la source
6
Personnellement, je préfère le modèle mental où, dans la première étape, vous construisez un arbre d'expression en utilisant la priorité et l'associativité. Et dans la deuxième étape, évaluez récursivement cet arbre en commençant par la racine. L'évaluation d'un nœud étant: Evaluez les nœuds enfants immédiats de gauche à droite, puis la note elle-même. | Un avantage de ce modèle est qu'il gère trivialement le cas où les opérateurs binaires ont un effet secondaire. Mais le principal avantage est qu'il s'adapte mieux à mon cerveau.
CodesInChaos
Ai-je raison de dire que C ++ ne garantit pas cela? Et Python?
Neil G
2
@Neil: C ++ ne garantit rien sur l'ordre d'évaluation, et ne l'a jamais fait. (Pas plus que C.) Python le garantit strictement par ordre de préséance; contrairement à tout le reste, l'affectation est R2L.
Donal Fellows
2
@aroth pour moi, vous semblez simplement confus. Et les règles de préséance impliquent seulement que les enfants doivent être évalués avant les parents. Mais ils ne disent rien sur l'ordre dans lequel les enfants sont évalués. Java et C # ont choisi de gauche à droite, C et C ++ ont choisi un comportement non défini.
CodesInChaos
6
@noober: OK, considérez: M (A () + B (), C () * D (), E () + F ()). Votre souhait est que les sous-expressions soient évaluées dans quel ordre? Devrait-on évaluer C () et D () avant A (), B (), E () et F () parce que la multiplication est plus prioritaire que l'addition? Il est facile de dire que "évidemment" l'ordre devrait être différent. Il est un peu plus difficile d'élaborer une règle qui couvre tous les cas. Les concepteurs de C # et Java ont choisi une règle simple et facile à expliquer: "aller de gauche à droite". Quelle est votre proposition de remplacement et pourquoi pensez-vous que votre règle est meilleure?
Eric Lippert
32

La réponse magistrale d'Eric Lippert n'est cependant pas vraiment utile car elle parle d'une langue différente. Il s'agit de Java, où la spécification du langage Java est la description définitive de la sémantique. En particulier, le §15.26.1 est pertinent car il décrit l'ordre d'évaluation de l' =opérateur (nous savons tous qu'il est associatif à droite, oui?). Réduire un peu les choses qui nous intéressent dans cette question:

Si l'expression d'opérande de gauche est une expression d'accès au tableau ( §15.13 ), de nombreuses étapes sont nécessaires:

  • Tout d'abord, la sous-expression de référence de tableau de l'expression d'accès au tableau d'opérande de gauche est évaluée. Si cette évaluation se termine brusquement, alors l'expression d'affectation se termine brusquement pour la même raison; la sous-expression d'index (de l'expression d'accès au tableau d'opérandes de gauche) et l'opérande de droite ne sont pas évaluées et aucune affectation ne se produit.
  • Sinon, la sous-expression d'index de l'expression d'accès au tableau d'opérandes de gauche est évaluée. Si cette évaluation se termine brusquement, l'expression d'affectation se termine brusquement pour la même raison et l'opérande de droite n'est pas évalué et aucune affectation ne se produit.
  • Sinon, l'opérande de droite est évalué. Si cette évaluation se termine brusquement, l'expression d'affectation se termine brusquement pour la même raison et aucune affectation ne se produit.

[… Il décrit ensuite la signification réelle de la mission elle-même, que nous pouvons ignorer ici par souci de concision…]

En bref, Java a un ordre d'évaluation très étroitement défini qui est à peu près exactement de gauche à droite dans les arguments de tout appel d'opérateur ou de méthode. Les affectations de tableaux sont l'un des cas les plus complexes, mais même là, il reste L2R. (Le JLS vous recommande ne pas écrire de code qui a besoin de ces sortes de contraintes sémantiques complexes , et moi aussi: vous pouvez avoir plus qu'assez de problèmes avec une seule affectation par instruction!)

C et C ++ sont définitivement différents de Java dans ce domaine: leurs définitions de langage laissent délibérément l'ordre d'évaluation indéfini pour permettre davantage d'optimisations. C # ressemble à Java apparemment, mais je ne connais pas assez bien sa littérature pour pouvoir pointer vers la définition formelle. (Cela varie vraiment selon la langue, Ruby est strictement L2R, tout comme Tcl - bien qu'il manque un opérateur d'affectation en soi pour des raisons non pertinentes ici - et Python est L2R mais R2L en ce qui concerne l'affectation , ce que je trouve étrange mais là vous allez .)

Boursiers Donal
la source
11
Donc, ce que vous dites est que la réponse d'Eric est fausse parce que Java la définit spécifiquement comme étant exactement ce qu'il a dit?
configurateur
8
La règle pertinente en Java (et C #) est "les sous-expressions sont évaluées de gauche à droite" - Il me semble qu'il parle des deux.
configurateur
2
Un peu confus ici - est-ce que cela rend la réponse ci-dessus d'Eric Lippert moins vraie, ou est-ce simplement une référence spécifique pour expliquer pourquoi c'est vrai?
GreenieMeanie
6
@Greenie: La réponse d'Eric est vraie, mais comme je l'ai dit, vous ne pouvez pas prendre un aperçu d'une langue dans ce domaine et l'appliquer à une autre sans faire attention. J'ai donc cité la source définitive.
Donal Fellows
1
ce qui est étrange, c'est que la main droite est évaluée avant que la variable de gauche ne soit résolue; in a[-1]=c, cest évalué, avant -1est reconnu comme invalide.
ZhongYu
5
a[b] = b = 0;

1) l'opérateur d'indexation de tableau a une priorité plus élevée que l'opérateur d'affectation (voir cette réponse ):

(a[b]) = b = 0;

2) Selon 15.26. Opérateurs d'affectation de JLS

Il existe 12 opérateurs d'affectation; tous sont syntaxiquement associatifs à droite (ils se regroupent de droite à gauche). Ainsi, a = b = c signifie a = (b = c), qui attribue la valeur de c à b puis attribue la valeur de b à a.

(a[b]) = (b=0);

3) Selon 15.7. Ordre d'évaluation du JLS

Le langage de programmation Java garantit que les opérandes des opérateurs semblent être évalués dans un ordre d'évaluation spécifique, à savoir, de gauche à droite.

et

L'opérande de gauche d'un opérateur binaire semble être entièrement évalué avant qu'une partie de l'opérande de droite ne soit évaluée.

Donc:

a) (a[b])évalué en premier àa[1]

b) puis (b=0)évalué à0

c) (a[1] = 0)évalué en dernier

bup
la source
1

Votre code équivaut à:

int[] a = {4,4};
int b = 1;
c = b;
b = 0;
a[c] = b;

ce qui explique le résultat.

Jérôme Verstrynge
la source
7
La question est de savoir pourquoi c'est le cas.
Mat
@Mat La réponse est parce que c'est ce qui se passe sous le capot compte tenu du code fourni dans la question. C'est ainsi que se déroule l'évaluation.
Jérôme Verstrynge
1
Oui je sais. Je ne répond toujours pas à la question par l'OMI, c'est pourquoi c'est ainsi que se déroule l'évaluation.
Mat
1
@Mat 'Pourquoi est-ce ainsi que l'évaluation se déroule?' n'est pas la question posée. «comment l'évaluation s'est passée? est la question posée.
Jérôme Verstrynge
1
@JVerstry: En quoi ne sont-ils pas équivalents? La sous-expression de référence de tableau de l'opérande de gauche est l'opérande le plus à gauche. Donc, dire "faire le plus à gauche en premier" revient exactement à dire "faire d'abord la référence au tableau". Si les auteurs de la spécification Java ont choisi d'être inutilement verbeux et redondants pour expliquer cette règle particulière, tant mieux pour eux; ce genre de chose est déroutant et devrait être plus plutôt que moins verbeux. Mais je ne vois pas en quoi ma caractérisation concise est sémantiquement différente de leur caractérisation prolixe.
Eric Lippert
0

Prenons un autre exemple plus détaillé ci-dessous.

En règle générale:

Il est préférable d'avoir un tableau des règles d'ordre de préséance et d'associativité disponible à lire lors de la résolution de ces questions, par exemple http://introcs.cs.princeton.edu/java/11precedence/

Voici un bon exemple:

System.out.println(3+100/10*2-13);

Question: Quelle est la sortie de la ligne ci-dessus?

Réponse: appliquer les règles de préséance et d'associativité

Étape 1: Selon les règles de priorité: les opérateurs / et * ont priorité sur les opérateurs + -. Par conséquent, le point de départ pour exécuter cette équation sera réduit à:

100/10*2

Étape 2: Selon les règles et la priorité: / et * sont égaux en priorité.

Comme les opérateurs / et * sont égaux en priorité, nous devons examiner l'associativité entre ces opérateurs.

Selon les RÈGLES D'ASSOCIATIVITÉ de ces deux opérateurs particuliers, nous commençons à exécuter l'équation de GAUCHE À DROITE, c'est-à-dire que 100/10 est exécuté en premier:

100/10*2
=100/10
=10*2
=20

Étape 3: L'équation est maintenant dans l'état d'exécution suivant:

=3+20-13

Selon les règles et la priorité: + et - sont égaux en priorité.

Nous devons maintenant regarder l'associativité entre les opérateurs + et - opérateurs. Selon l'associativité de ces deux opérateurs particuliers, nous commençons à exécuter l'équation de GAUCHE à DROITE, c'est-à-dire que 3 + 20 est exécuté en premier:

=3+20
=23
=23-13
=10

10 est la sortie correcte une fois compilée

Encore une fois, il est important d'avoir un tableau des règles d'ordre de préséance et d'associativité avec vous lorsque vous résolvez ces questions, par exemple http://introcs.cs.princeton.edu/java/11precedence/

Mark Burleigh
la source
1
Vous dites que "l'associativité entre les opérateurs + et - opérateurs" est "DROITE À GAUCHE". Essayez d'utiliser cette logique et évaluez 10 - 4 - 3.
Pshemo
1
Je soupçonne que cette erreur peut être causée par le fait qu'en haut de introcs.cs.princeton.edu/java/11precedence + est un opérateur unaire (qui a une associativité de droite à gauche), mais additif + et - ont juste comme multiplicatif * /% à gauche à la bonne associativité.
Pshemo
Bien repéré et modifié en conséquence, merci Pshemo
Mark Burleigh
Cette réponse explique la préséance et l'associativité, mais, comme l' explique Eric Lippert , la question porte sur l'ordre d'évaluation, qui est très différent. En fait, cela ne répond pas à la question.
Fabio dit réintégrer Monica le