Perché x == (x = y) non è uguale a (x = y) == x?


207

Considera il seguente esempio:

class Quirky {
    public static void main(String[] args) {
        int x = 1;
        int y = 3;

        System.out.println(x == (x = y)); // false
        x = 1; // reset
        System.out.println((x = y) == x); // true
     }
}

Non sono sicuro se nella specifica del linguaggio Java sia presente un elemento che impone il caricamento del valore precedente di una variabile per il confronto con il lato destro ( x = y) che, per ordine implicito tra parentesi, dovrebbe essere calcolato per primo.

Perché la prima espressione valuta false, ma la seconda valuta true? Mi sarei aspettato (x = y)di essere valutato per primo, quindi sarebbe comparabile xcon se stesso ( 3) e restituito true.


Questa domanda è diversa dall'ordine di valutazione delle sottoespressioni in un'espressione Java in quanto non xè sicuramente una "sottoespressione" qui. Deve essere caricato per il confronto piuttosto che essere "valutato". La domanda è specifica di Java e l'espressione x == (x = y), a differenza di costrutti poco pratici e impraticabili comunemente realizzati per domande di intervista complicate, proveniva da un vero progetto. Doveva essere una sostituzione di una riga per il linguaggio di confronto e sostituzione

int oldX = x;
x = y;
return oldX == y;

che, essendo ancora più semplice dell'istruzione x86 CMPXCHG, meritava un'espressione più breve in Java.


62
Il lato sinistro viene sempre valutato prima del lato destro. Le parentesi non fanno la differenza.
Louis Wasserman,

11
La valutazione dell'espressione x = yè sicuramente pertinente e causa l'effetto collaterale ximpostato sul valore di y.
Louis Wasserman,

50
Fai un favore a te stesso e ai tuoi compagni di squadra e non mescolare la mutazione di stato nella stessa linea dell'esame di stato. Ciò riduce drasticamente la leggibilità del codice. (Ci sono alcuni casi in cui è assolutamente necessario a causa dei requisiti di atomicità, ma le funzioni per quelli già esistenti e il loro scopo sarebbero immediatamente riconosciuti.)
jpmc26

50
La vera domanda è perché vuoi scrivere codice in questo modo.
Klut,

26
La chiave della tua domanda è la tua falsa convinzione che le parentesi implicano un ordine di valutazione. Questa è una convinzione comune a causa del modo in cui ci viene insegnato la matematica nella scuola elementare e perché alcuni libri di programmazione per principianti continuano a sbagliare , ma è una falsa convinzione. Questa è una domanda abbastanza frequente. Potresti trarre beneficio dalla lettura dei miei articoli sull'argomento; si tratta di C # ma si applicano a Java: ericlippert.com/2008/05/23/precedence-vs-associativity-vs-order ericlippert.com/2009/08/10/precedence-vs-order-redux
Eric Lippert

Risposte:


97

che, secondo l'ordine implicito tra parentesi, dovrebbe essere calcolato per primo

No. È un'idea sbagliata comune che le parentesi abbiano effetti (generali) sul calcolo o sull'ordine di valutazione. Costringono solo le parti della tua espressione in un particolare albero, legando gli operandi giusti alle operazioni giuste per il lavoro.

(E, se non li usi, queste informazioni provengono dalla "precedenza" e dall'associatività degli operatori, qualcosa che è il risultato di come viene definito l'albero della sintassi del linguaggio. In realtà, questo è ancora esattamente come funziona quando usare le parentesi, ma semplifichiamo e diciamo che non ci baseremo su nessuna regola di precedenza).

Una volta fatto (cioè una volta che il tuo codice è stato analizzato in un programma), quegli operandi devono ancora essere valutati e ci sono regole separate su come farlo: dette regole (come ci ha mostrato Andrew) affermano che l'LHS di ogni operazione viene valutato per primo in Java.

Si noti che questo non è il caso in tutte le lingue; ad esempio, in C ++, a meno che non si stia utilizzando un operatore di cortocircuito come &&o ||, l'ordine di valutazione degli operandi non è generalmente specificato e non si deve fare affidamento su di esso in entrambi i casi.

Gli insegnanti devono smettere di spiegare la precedenza dell'operatore usando frasi fuorvianti come "questo fa sì che l'aggiunta avvenga per prima". Data un'espressione, x * y + zla spiegazione corretta sarebbe "la precedenza dell'operatore fa sì che l'aggiunta avvenga tra x * ye z, piuttosto che tra ye z", senza menzionare alcun "ordine".


6
Vorrei che i miei insegnanti avessero fatto una certa separazione tra la matematica sottostante e la sintassi che rappresentavano, come se passassimo una giornata con numeri romani o notazione polacca o altro e vedessimo che l'aggiunta ha le stesse proprietà. Abbiamo imparato l'associatività e tutte quelle proprietà nella scuola media, quindi c'era molto tempo.
Giovanni P,

1
Sono contento che tu abbia detto che questa regola non vale per tutte le lingue. Inoltre, se una delle parti ha un altro effetto collaterale, come scrivere su un file o leggere l'ora corrente, è (anche in Java) non definita nell'ordine in cui si verifica. Tuttavia, il risultato del confronto sarà come se fosse valutato da sinistra a destra (in Java). Un altro aspetto: un bel po 'di lingue semplicemente impediscono di mescolare compiti e confronti in questo modo dalle regole di sintassi e il problema non si presenta.
Abele,

5
@JohnP: peggiora. 5 * 4 significa 5 + 5 + 5 + 5 o 4 + 4 + 4 + 4 + 4? Alcuni insegnanti insistono sul fatto che solo una di queste scelte sia giusta.
Brian,

3
@Brian Ma ... ma ... la moltiplicazione dei numeri reali è commutativa!
Razze di leggerezza in orbita dal

2
Nel mio mondo di pensiero, una coppia di parentesi rappresenta "è necessario per". Calcolando ´a * (b + c) ´, le parentesi esprimono che il risultato dell'addizione è necessario per la moltiplicazione. Le preferenze implicite dell'operatore possono essere espresse dai genitori, ad eccezione delle regole LHS-first o RHS-first. (È vero?) @Brian In matematica ci sono alcuni rari casi in cui la moltiplicazione può essere sostituita da aggiunte ripetute, ma ciò non è di certo sempre vero (a partire da numeri complessi ma non limitati a). Quindi i tuoi educatori dovrebbero davvero avere un occhio su ciò che dicono le persone ....
syck

164

==è un operatore di uguaglianza binaria .

L'operando di sinistra di un operatore binario sembra essere valutato completamente prima di valutare qualsiasi parte dell'operando di destra .

Specifiche di Java 11> Ordine di valutazione> Valuta prima l'operando per mano sinistra


42
La dicitura "sembra essere" non suona come se fossero sicuri, grazie.
Lister,

86
"sembra essere" significa che la specifica non richiede che le operazioni siano effettivamente eseguite in quell'ordine in ordine cronologico, ma richiede che tu ottenga lo stesso risultato che otterresti se fosse.
Robyn,

24
@MrLister "sembra essere" sembra essere una cattiva scelta di parole da parte loro. Per "apparire" significano "manifest come fenomeno per lo sviluppatore". "è efficacemente" potrebbe essere una frase migliore.
Kelvin,

18
nella comunità C ++ questo equivale alla regola "come se" ... l'operando deve comportarsi "come se" fosse implementato secondo le seguenti regole, anche se tecnicamente non lo è.
Michael Edenfield,

2
@Kelvin Sono d'accordo, avrei scelto quella parola, piuttosto che "sembra essere".
MC Emperor

149

Come ha detto LouisWasserman, l'espressione viene valutata da sinistra a destra. E a java non importa cosa faccia effettivamente "valutare", si preoccupa solo di generare un valore (non volatile, finale) con cui lavorare.

//the example values
x = 1;
y = 3;

Quindi per calcolare il primo output di System.out.println(), si procede come segue:

x == (x = y)
1 == (x = y)
1 == (x = 3) //assign 3 to x, returns 3
1 == 3
false

e per calcolare il secondo:

(x = y) == x
(x = 3) == x //assign 3 to x, returns 3
3 == x
3 == 3
true

Si noti che il secondo valore verrà sempre valutato come vero, indipendentemente dai valori iniziali di xe y, poiché si sta effettivamente confrontando l'assegnazione di un valore alla variabile a cui è assegnato e, a = be bsarà valutato in quell'ordine, sarà sempre lo stesso per definizione.


"Da sinistra a destra" è anche vero in matematica, a proposito, proprio quando si arriva a una parentesi o a una precedenza, si esegue l'iterazione all'interno di esse e si valuta tutto all'interno da sinistra a destra prima di andare oltre sul livello principale. Ma la matematica non lo farebbe mai; la distinzione conta solo perché questa non è un'equazione ma un'operazione combinata, facendo sia un compito che un'equazione in un colpo solo . Non lo farei mai perché la leggibilità è scarsa, a meno che non stia facendo code golf o non stia cercando un modo per ottimizzare le prestazioni, e quindi ci sarebbero commenti.
Harper - Ripristina Monica il

25

Non sono sicuro se nella specifica del linguaggio Java sia presente un elemento che impone il caricamento del valore precedente di una variabile ...

C'è. La prossima volta che non si sa bene cosa dice la specifica, si prega di leggere la specifica e quindi porre la domanda se non è chiara.

... il lato destro (x = y)che, per ordine implicito tra parentesi, dovrebbe essere calcolato per primo.

Questa affermazione è falsa. Le parentesi non implicano un ordine di valutazione . In Java, l'ordine di valutazione è da sinistra a destra, indipendentemente dalle parentesi. Le parentesi determinano dove si trovano i limiti della sottoespressione, non l'ordine di valutazione.

Perché la prima espressione viene valutata come falsa, ma la seconda viene valutata come vera?

La regola per l' ==operatore è: valutare il lato sinistro per produrre un valore, valutare il lato destro per produrre un valore, confrontare i valori, il confronto è il valore dell'espressione.

In altre parole, il significato di expr1 == expr2è sempre lo stesso di se tu avessi scritto temp1 = expr1; temp2 = expr2;e valutato temp1 == temp2.

La regola per l' =operatore con una variabile locale sul lato sinistro è: valutare il lato sinistro per produrre una variabile, valutare il lato destro per produrre un valore, eseguire l'assegnazione, il risultato è il valore assegnato.

Quindi mettilo insieme:

x == (x = y)

Abbiamo un operatore di confronto. Valuta il lato sinistro per produrre un valore: otteniamo il valore corrente di x. Valuta il lato destro: questo è un compito, quindi valutiamo il lato sinistro per produrre una variabile - la variabile x- valutiamo il lato destro - il valore corrente di y- assegnalo a x, e il risultato è il valore assegnato. Confrontiamo quindi il valore originale di xcon il valore assegnato.

Puoi fare (x = y) == xcome esercizio. Ancora una volta, ricorda, tutte le regole per valutare il lato sinistro si verificano prima di tutte le regole per valutare il lato destro .

Mi sarei aspettato di valutare prima (x = y), quindi avrebbe confrontato x con se stesso (3) e sarebbe tornato vero.

Le tue aspettative si basano su una serie di credenze errate sulle regole di Java. Spero che ora tu abbia credenze corrette e in futuro ti aspetteresti cose vere.

Questa domanda è diversa da "ordine di valutazione delle sottoespressioni in un'espressione Java"

Questa affermazione è falsa. Questa domanda è totalmente germana.

x non è sicuramente una "sottoespressione" qui.

Anche questa affermazione è falsa. È una sottoespressione due volte in ogni esempio.

Deve essere caricato per il confronto piuttosto che essere "valutato".

Non ho idea di cosa significhi.

Apparentemente hai ancora molte false credenze. Il mio consiglio è di leggere le specifiche fino a quando le vostre false credenze vengono sostituite da vere credenze.

La domanda è specifica di Java e l'espressione x == (x = y), a differenza dei costrutti impraticabili di vasta portata comunemente realizzati per le domande difficili dell'intervista, proveniva da un vero progetto.

La provenienza dell'espressione non è rilevante per la domanda. Le regole per tali espressioni sono chiaramente descritte nelle specifiche; leggilo!

Doveva essere una sostituzione di una riga per il linguaggio di confronto e sostituzione

Dal momento che la sostituzione di una riga ha causato molta confusione in te, lettore del codice, suggerirei che è stata una scelta sbagliata. Rendere il codice più conciso ma più difficile da capire non è una vittoria. È improbabile che il codice sia più veloce.

Per inciso, C # ha confrontato e sostituito come metodo di libreria, che può essere ridotto a un'istruzione macchina. Credo che Java non abbia un tale metodo, in quanto non può essere rappresentato nel sistema di tipo Java.


8
Se qualcuno potesse passare attraverso l'intero JLS, allora non ci sarebbe motivo di pubblicare libri Java e almeno la metà di questo sito sarebbe inutile.
John McClane,

8
@JohnMcClane: ti assicuro che non ci sono difficoltà nel passare attraverso tutte le specifiche, ma non è necessario. La specifica Java inizia con un utile "sommario" che ti aiuterà a raggiungere rapidamente le parti che ti interessano di più. È anche online e può essere ricercato tramite parole chiave. Detto questo, hai ragione: ci sono molte buone risorse che ti aiuteranno a imparare come funziona Java; il mio consiglio è che tu li usi!
Eric Lippert,

8
Questa risposta è inutilmente condiscendente e maleducata. Ricorda: sii gentile .
walen

7
@LuisG .: Non è prevista o implicita alcuna condiscendenza; siamo tutti qui per imparare gli uni dagli altri e non sto raccomandando nulla che non ho fatto da solo quando ero un principiante. Né è maleducato. Identificare in modo chiaro e inequivocabile le loro false credenze è una gentilezza per il poster originale . Nascondersi dietro "educazione" e consentire alle persone di continuare ad avere false credenze non è di aiuto , e rafforza le cattive abitudini di pensiero .
Eric Lippert,

5
@LuisG .: Scrivevo un blog sulla progettazione di JavaScript, e i commenti più utili che io abbia mai ricevuto sono stati da Brendan sottolineando chiaramente e senza ambiguità dove ho sbagliato. È stato grandioso e l'ho apprezzato molto, perché ho vissuto i successivi 20 anni della mia vita senza ripetere quell'errore nel mio lavoro, o peggio, insegnandolo ad altri. Mi ha anche dato l'opportunità di correggere quelle stesse false credenze negli altri usando me stesso come esempio di come le persone arrivano a credere alle cose false.
Eric Lippert,

16

È correlato alla precedenza degli operatori e al modo in cui gli operatori vengono valutati.

Le parentesi '()' hanno una precedenza più elevata e associatività da sinistra a destra. L'uguaglianza '==' viene dopo in questa domanda e ha associatività da sinistra a destra. L'assegnazione '=' arriva per ultima e ha associatività da destra a sinistra.

Lo stack utilizza il sistema per valutare l'espressione. L'espressione viene valutata da sinistra a destra.

Ora arriva alla domanda originale:

int x = 1;
int y = 3;
System.out.println(x == (x = y)); // false

La prima x (1) verrà spinta per impilare. quindi inner (x = y) verrà valutato e spinto per impilare con il valore x (3). Ora x (1) verrà confrontato con x (3), quindi il risultato è falso.

x = 1; // reset
System.out.println((x = y) == x); // true

Qui, verrà valutato (x = y), ora il valore x diventa 3 e x (3) verrà inserito nello stack. Ora x (3) con valore modificato dopo l'uguaglianza verrà spinto per impilare. Ora l'espressione verrà valutata ed entrambe saranno uguali, quindi il risultato è vero.


12

Non é la stessa cosa. Il lato sinistro verrà sempre valutato prima del lato destro e le parentesi non specificano un ordine di esecuzione, ma un raggruppamento di comandi.

Con:

      x == (x = y)

In pratica stai facendo lo stesso di:

      x == y

E x avrà il valore di y dopo il confronto.

Mentre con:

      (x = y) == x

In pratica stai facendo lo stesso di:

      x == x

Dopo che x ha preso il valore di y . E tornerà sempre vero .


9

Nel primo test che stai controllando fa 1 == 3.

Nel secondo test il tuo controllo fa 3 == 3.

(x = y) assegna il valore e quel valore viene testato. Nel primo esempio, x = 1 viene quindi assegnato a x 3. 1 == 3?

In quest'ultimo caso, x è assegnato a 3 e ovviamente è ancora 3. Fa 3 == 3?


8

Considera questo altro esempio, forse più semplice:

int x = 1;
System.out.println(x == ++x); // false
x = 1; // reset
System.out.println(++x == x); // true

Qui, l'operatore di pre-incremento ++xdeve essere applicato prima di effettuare il confronto, proprio come (x = y)nell'esempio deve essere calcolato prima del confronto.

Tuttavia, la valutazione dell'espressione avviene ancora da sinistra → a → destra , quindi il primo confronto è in realtà 1 == 2mentre il secondo è 2 == 2.
La stessa cosa accade nel tuo esempio.


8

Le espressioni vengono valutate da sinistra a destra. In questo caso:

int x = 1;
int y = 3;

x == (x = y)) // false
x ==    t

- left x = 1
- let t = (x = y) => x = 3
- x == (x = y)
  x == t
  1 == 3 //false

(x = y) == x); // true
   t    == x

- left (x = y) => x = 3
           t    =      3 
-  (x = y) == x
-     t    == x
-     3    == 3 //true

5

Fondamentalmente la prima istruzione x aveva il suo valore 1 Quindi Java confronta 1 == con la nuova variabile x che non sarà la stessa

Nel secondo hai detto x = y che significa che il valore di x è cambiato e quindi quando lo chiami di nuovo sarà lo stesso valore quindi perché è vero e x == x


4

== è un operatore di uguaglianza di confronto e funziona da sinistra a destra.

x == (x = y);

qui il vecchio valore assegnato di x viene confrontato con il nuovo valore assegnato di x, (1 == 3) // false

(x = y) == x;

Mentre qui il nuovo valore di assegnazione di x viene confrontato con il nuovo valore di partecipazione di x assegnato ad esso appena prima del confronto, (3 == 3) // true

Ora considera questo

    System.out.println((8 + (5 * 6)) * 9);
    System.out.println(8 + (5 * 6) * 9);
    System.out.println((8 + 5) * 6 * 9);
    System.out.println((8 + (5) * 6) * 9);
    System.out.println(8 + 5 * 6 * 9);

Produzione:

342

278

702

342

278

Pertanto, le parentesi svolgono il suo ruolo principale nelle espressioni aritmetiche non solo nelle espressioni di confronto.


1
La conclusione è sbagliata Il comportamento non è diverso tra operatori aritmetici e comparativi. x + (x = y)e (x = y) + xmostrerebbe un comportamento simile all'originale con operatori di confronto.
JJJ

1
@JJJ In x + (x = y) e (x = y) + x non esiste alcun confronto, sta semplicemente assegnando il valore y a x e aggiungendolo a x.
Nisrin Dhoondia,

1
... sì, questo è il punto. "Le parentesi svolgono il suo ruolo principale nelle espressioni aritmetiche non solo nelle espressioni di confronto" è errata perché non vi è alcuna differenza tra espressioni aritmetiche e espressioni di confronto.
JJJ il

2

La cosa qui è l'ordine di precedenza degli operatori aritmici / operatori relazionali tra i due operatori =rispetto a ==quello dominante è ==(Operatori relazionali domina) come precede gli =operatori di assegnazione. Nonostante la precedenza, l'ordine di valutazione è LTR (SINISTRA A DESTRA) la precedenza viene visualizzata dopo l'ordine di valutazione. Pertanto, indipendentemente dalla valutazione dei vincoli è LTR.


1
La risposta è sbagliata La precedenza dell'operatore non influisce sull'ordine di valutazione. Leggi alcune delle risposte più votate per una spiegazione, in particolare questa .
JJJ,

1
Corretto, in realtà è il modo in cui ci viene insegnato che l'illusione delle restrizioni sulla precedenza viene in tutte queste cose, ma correttamente indicato che non ha alcun impatto perché l'ordine di valutazione rimane da sinistra a destra
Himanshu Ahuja,

-1

È facile nel secondo confronto a sinistra è l'assegnazione dopo aver assegnato y a x (a sinistra) confrontando quindi 3 == 3. Nel primo esempio si sta confrontando x = 1 con il nuovo assegnato x = 3. Sembra che vengono sempre prese le dichiarazioni di lettura dello stato corrente da sinistra a destra di x.


-2

Il tipo di domanda che hai posto è un'ottima domanda se vuoi scrivere un compilatore Java o testare i programmi per verificare che un compilatore Java funzioni correttamente. In Java, queste due espressioni devono produrre i risultati che hai visto. In C ++, ad esempio, non è necessario, quindi se qualcuno riutilizzava parti di un compilatore C ++ nel proprio compilatore Java, in teoria si potrebbe scoprire che il compilatore non si comporta come dovrebbe.

Come sviluppatore di software, scrivere codice che sia leggibile, comprensibile e gestibile, entrambe le versioni del tuo codice sarebbero considerate orribili. Per capire cosa fa il codice, bisogna sapere esattamente come viene definito il linguaggio Java. Qualcuno che scrive sia codice Java che C ++ rabbrividirebbe guardando il codice. Se devi chiederti perché una singola riga di codice fa quello che fa, allora dovresti evitare quel codice. (Suppongo e spero che i ragazzi che hanno risposto correttamente alla tua domanda "perché" evitino anch'essi quel codice).


"Per capire cosa fa il codice, bisogna sapere esattamente come viene definito il linguaggio Java." E se ogni collega lo considerasse un senso comune?
BinaryTreeee,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.