Quais são as regras para a ordem de avaliação em Java?

86

Estou lendo algum texto Java e obtive o seguinte código:

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

No texto, o autor não deu uma explicação clara e o efeito da última linha é: a[1] = 0;

Não tenho tanta certeza de entender: como aconteceu a avaliação?

ipkiss
fonte
19
A confusão abaixo sobre porque isso acontece significa, a propósito, que você nunca deve fazer isso, pois muitas pessoas terão que pensar sobre o que realmente faz, ao invés de parecer óbvio.
Martijn
A resposta adequada para qualquer pergunta como essa é "não faça isso!" A atribuição deve ser tratada como uma declaração; o uso de atribuição como uma expressão que retorna um valor deve gerar um erro do compilador ou, no mínimo, um aviso. Não faça isso.
Mason Wheeler

Respostas:

173

Deixe-me dizer isso muito claramente, porque as pessoas não entendem isso o tempo todo:

A ordem de avaliação das subexpressões é independente da associatividade e da precedência . A associatividade e a precedência determinam em que ordem os operadores são executados, mas não determinam em que ordem as subexpressões são avaliadas. Sua pergunta é sobre a ordem em que as subexpressões são avaliadas.

Considere A() + B() + C() * D(). A multiplicação tem precedência mais alta do que a adição, e a adição é associativa à esquerda, então isso é equivalente a (A() + B()) + (C() * D()) Mas saber isso apenas informa que a primeira adição acontecerá antes da segunda adição e que a multiplicação acontecerá antes da segunda adição. Ele não diz em que ordem A (), B (), C () e D () serão chamados! (Também não informa se a multiplicação acontece antes ou depois da primeira adição.) Seria perfeitamente possível obedecer às regras de precedência e associatividade compilando isso como:

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

Todas as regras de precedência e associatividade são seguidas lá - a primeira adição acontece antes da segunda adição, e a multiplicação acontece antes da segunda adição. É claro que podemos fazer as chamadas para A (), B (), C () e D () em qualquer ordem e ainda obedecer às regras de precedência e associatividade!

Precisamos de uma regra não relacionada às regras de precedência e associatividade para explicar a ordem em que as subexpressões são avaliadas. A regra relevante em Java (e C #) é "subexpressões são avaliadas da esquerda para a direita". Visto que A () aparece à esquerda de C (), A () é avaliado primeiro, independentemente do fato de que C () está envolvido em uma multiplicação e A () está envolvido apenas em uma adição.

Agora você tem informações suficientes para responder à sua pergunta. Nas a[b] = b = 0regras de associatividade, diga que isso é, a[b] = (b = 0);mas isso não significa que o b=0primeiro é executado! As regras de precedência dizem que a indexação é uma precedência mais alta do que a atribuição, mas isso não significa que o indexador é executado antes da atribuição mais à direita .

(ATUALIZAÇÃO: uma versão anterior desta resposta tinha algumas omissões pequenas e praticamente sem importância na seção a seguir que eu corrigi. Também escrevi um artigo de blog descrevendo por que essas regras são razoáveis ​​em Java e C # aqui: https: // ericlippert.com/2019/01/18/indexer-error-cases/ )

A precedência e a associatividade apenas nos dizem que a atribuição de zero a bdeve acontecer antes da atribuição a a[b], porque a atribuição de zero calcula o valor atribuído na operação de indexação. A precedência e a associatividade sozinhas não dizem nada sobre se o a[b]é avaliado antes ou depois do b=0.

Novamente, isso é exatamente o mesmo que: A()[B()] = C()- Tudo o que sabemos é que a indexação deve acontecer antes da atribuição. Não sabemos se A (), B () ou C () é executado primeiro com base na precedência e na associatividade . Precisamos de outra regra para nos dizer isso.

A regra é, novamente, "quando você tiver uma escolha sobre o que fazer primeiro, vá sempre da esquerda para a direita". No entanto, há uma ruga interessante neste cenário específico. O efeito colateral de uma exceção lançada é causado por uma coleção nula ou índice fora do intervalo considerado parte do cálculo do lado esquerdo da atribuição ou parte do cálculo da própria atribuição? Java escolhe o último. (Claro, esta é uma distinção que só importa se o código já estiver errado , porque o código correto não cancela a referência de nulo ou passa um índice incorreto em primeiro lugar.)

Então o que acontece?

  • O a[b]está à esquerda do b=0, portanto, o a[b]é executado primeiro , resultando em a[1]. No entanto, a verificação da validade desta operação de indexação está atrasada.
  • Então b=0acontece.
  • Então, a verificação que aé válida e a[1]está dentro do intervalo acontece
  • A atribuição do valor a a[1]acontece por último.

Portanto, embora neste caso específico haja algumas sutilezas a serem consideradas para aqueles raros casos de erro que não deveriam estar ocorrendo no código correto em primeiro lugar, em geral você pode raciocinar: as coisas à esquerda acontecem antes das coisas à direita . Essa é a regra que você está procurando. Falar de precedência e associatividade é confuso e irrelevante.

As pessoas entendem isso errado o tempo todo , até mesmo pessoas que deveriam saber disso. Eu editei muitos livros de programação que declaravam as regras incorretamente, então não é surpresa que muitas pessoas tenham crenças completamente incorretas sobre a relação entre precedência / associatividade e ordem de avaliação - ou seja, que na realidade não existe tal relação ; eles são independentes.

Se este tópico interessar a você, veja meus artigos sobre o assunto para leitura adicional:

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

Eles são sobre C #, mas a maioria dessas coisas se aplica igualmente bem ao Java.

Eric Lippert
fonte
6
Pessoalmente, prefiro o modelo mental em que, na primeira etapa, você constrói uma árvore de expressão usando precedência e associatividade. E na segunda etapa avalie recursivamente essa árvore começando com a raiz. Com a avaliação de um nó sendo: Avalie os nós filhos imediatos da esquerda para a direita e a seguir a própria nota. | Uma vantagem desse modelo é que ele trata trivialmente o caso em que os operadores binários têm um efeito colateral. Mas a principal vantagem é que simplesmente se ajusta melhor ao meu cérebro.
CodesInChaos
Estou certo de que C ++ não garante isso? E quanto ao Python?
Neil G
2
@Neil: C ++ não garante nada sobre a ordem de avaliação, e nunca garantiu. (Nem o C.) Python garante estritamente por ordem de precedência; ao contrário de tudo, a atribuição é R2L.
Donal Fellows
2
@aroth para mim você parece apenas confuso. E as regras de precedência implicam apenas que os filhos precisam ser avaliados antes dos pais. Mas eles nada dizem sobre a ordem em que as crianças são avaliadas. Java e C # escolheram da esquerda para a direita, C e C ++ escolheram comportamento indefinido.
CodesInChaos
6
@noober: OK, considere: M (A () + B (), C () * D (), E () + F ()). Seu desejo é que as subexpressões sejam avaliadas em que ordem? Devem C () e D () ser avaliados antes de A (), B (), E () e F () porque a multiplicação tem precedência mais alta do que a adição? É fácil dizer que "obviamente" a ordem deveria ser diferente. É muito mais difícil chegar a uma regra real que cubra todos os casos. Os designers de C # e Java escolheram uma regra simples e fácil de explicar: "vá da esquerda para a direita". Qual é a sua substituição proposta para ele e por que você acredita que sua regra é melhor?
Eric Lippert
32

A resposta magistral de Eric Lippert, no entanto, não é propriamente útil porque está falando sobre um idioma diferente. Este é o Java, onde a Especificação da linguagem Java é a descrição definitiva da semântica. Em particular, §15.26.1 é relevante porque descreve a ordem de avaliação do =operador (todos nós sabemos que é associativo à direita, sim?). Cortando um pouco nas partes que nos interessam nesta questão:

Se a expressão do operando à esquerda for uma expressão de acesso à matriz ( §15.13 ), muitas etapas são necessárias:

  • Em primeiro lugar, a subexpressão de referência de matriz da expressão de acesso à matriz do operando à esquerda é avaliada. Se essa avaliação for concluída abruptamente, a expressão de atribuição será concluída abruptamente pelo mesmo motivo; a subexpressão do índice (da expressão de acesso à matriz do operando à esquerda) e o operando à direita não são avaliados e nenhuma atribuição ocorre.
  • Caso contrário, a subexpressão de índice da expressão de acesso à matriz do operando à esquerda é avaliada. Se essa avaliação for concluída abruptamente, a expressão de atribuição será concluída abruptamente pelo mesmo motivo e o operando direito não será avaliado e nenhuma atribuição ocorrerá.
  • Caso contrário, o operando à direita é avaliado. Se essa avaliação for concluída abruptamente, a expressão de atribuição será concluída abruptamente pelo mesmo motivo e nenhuma atribuição ocorrerá.

[... em seguida, descreve o significado real da atribuição em si, que podemos ignorar aqui por brevidade ...]

Resumindo, o Java tem uma ordem de avaliação bem definida que é exatamente da esquerda para a direita nos argumentos de qualquer operador ou chamada de método. As atribuições de array são um dos casos mais complexos, mas mesmo assim ainda é L2R. (O JLS recomenda que você não escreva código que precise desses tipos de restrições semânticas complexas , e eu também: você pode ter problemas mais do que suficientes com apenas uma atribuição por instrução!)

C e C ++ são definitivamente diferentes de Java nesta área: suas definições de linguagem deixam a ordem de avaliação indefinida deliberadamente para permitir mais otimizações. C # é aparentemente como Java, mas não conheço sua literatura bem o suficiente para apontar para a definição formal. (Isso realmente varia de acordo com a linguagem, Ruby é estritamente L2R, assim como Tcl - embora falte um operador de atribuição per se por razões não relevantes aqui - e Python é L2R, mas R2L em relação à atribuição , o que eu acho estranho, mas aí está .)

Donal Fellows
fonte
11
Então, o que você está dizendo é que a resposta de Eric está errada porque Java a define especificamente para ser exatamente o que ele disse?
configurador de
8
A regra relevante em Java (e C #) é "subexpressões são avaliadas da esquerda para a direita" - parece-me que ele está falando sobre ambos.
configurador de
2
Um pouco confuso aqui - isso torna a resposta acima de Eric Lippert menos verdadeira ou está apenas citando uma referência específica de por que é verdade?
GreenieMeanie
5
@Greenie: A resposta de Eric é verdadeira, mas, como afirmei, você não pode obter uma visão de um idioma nesta área e aplicá-lo a outro sem ser cuidadoso. Então, citei a fonte definitiva.
Donal Fellows de
1
o estranho é que o lado direito é avaliado antes que a variável do lado esquerdo seja resolvida; em a[-1]=c, cé avaliado, antes de -1ser reconhecido como inválido.
ZhongYu
5
a[b] = b = 0;

1) o operador de indexação de matriz tem maior precedência do que o operador de atribuição (veja esta resposta ):

(a[b]) = b = 0;

2) De acordo com 15.26. Operadores de atribuição de JLS

Existem 12 operadores de atribuição; todos são sintaticamente associativos à direita (eles agrupam da direita para a esquerda). Assim, a = b = c significa a = (b = c), que atribui o valor de c a be, em seguida, atribui o valor de b a a.

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

3) De acordo com 15.7. Ordem de Avaliação de JLS

A linguagem de programação Java garante que os operandos dos operadores pareçam ser avaliados em uma ordem de avaliação específica, a saber, da esquerda para a direita.

e

O operando à esquerda de um operador binário parece ser totalmente avaliado antes que qualquer parte do operando à direita seja avaliada.

Assim:

a) (a[b])avaliado primeiro paraa[1]

b) então (b=0)avaliado para0

c) (a[1] = 0)avaliado por último

bup
fonte
1

Seu código é equivalente a:

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

o que explica o resultado.

Jérôme Verstrynge
fonte
7
A questão é por que esse é o caso.
Mat
@Mat A resposta é porque isso é o que acontece nos bastidores, considerando o código fornecido na pergunta. É assim que acontece a avaliação.
Jérôme Verstrynge
1
Sim eu conheço. Ainda não estou respondendo a pergunta via IMO, por isso é assim que acontece a avaliação.
Mat
1
@Mat 'Por que é assim que a avaliação acontece?' não é a pergunta feita. 'como a avaliação aconteceu?' é a pergunta feita.
Jérôme Verstrynge
1
@JVerstry: Como eles não são equivalentes? A subexpressão de referência de matriz do operando à esquerda é o operando mais à esquerda. Portanto, dizer "faça o mais à esquerda primeiro" é exatamente o mesmo que dizer "faça a referência ao array primeiro". Se os autores das especificações Java escolheram ser desnecessariamente prolixo e redundante ao explicar essa regra em particular, bom para eles; esse tipo de coisa é confuso e deveria ser mais do que menos prolixo. Mas não vejo como minha caracterização concisa é semanticamente diferente de sua caracterização prolixa.
Eric Lippert
0

Considere outro exemplo mais detalhado abaixo.

Como regra geral:

É melhor ter uma tabela das Regras da Ordem de Precedência e Associatividade disponível para leitura ao resolver essas questões, por exemplo, http://introcs.cs.princeton.edu/java/11precedence/

Aqui está um bom exemplo:

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

Pergunta: Qual é o resultado da linha acima?

Resposta: Aplique as Regras de Precedência e Associatividade

Etapa 1: De acordo com as regras de precedência: os operadores / e * têm prioridade sobre os operadores + -. Portanto, o ponto de partida para executar esta equação será reduzido a:

100/10*2

Etapa 2: De acordo com as regras e precedência: / e * são iguais em precedência.

Como os operadores / e * são iguais em precedência, precisamos examinar a associatividade entre esses operadores.

De acordo com as REGRAS DE ASSOCIATIVIDADE destes dois operadores em particular, começamos a executar a equação da ESQUERDA PARA A DIREITA, ou seja, 100/10 é executado primeiro:

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

Etapa 3: a equação agora está no seguinte estado de execução:

=3+20-13

De acordo com as regras e precedência: + e - são iguais em precedência.

Agora precisamos olhar para a associatividade entre os operadores + e - operadores. De acordo com a associatividade destes dois operadores particulares, começamos a executar a equação da ESQUERDA para a DIREITA, ou seja, 3 + 20 é executado primeiro:

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

10 é a saída correta quando compilado

Mais uma vez, é importante ter uma tabela das Regras da Ordem de Precedência e Associatividade ao resolver essas questões, por exemplo, http://introcs.cs.princeton.edu/java/11precedence/

Mark Burleigh
fonte
1
Você está dizendo que "associatividade entre os operadores + e - operadores" é "DIREITA PARA ESQUERDA". Tente usar essa lógica e avalie 10 - 4 - 3.
Pshemo
1
Eu suspeito que esse erro pode ser causado pelo fato de que no topo de introcs.cs.princeton.edu/java/11 a precedência + é um operador unário (que tem associatividade da direita para a esquerda), mas os aditivos + e - têm apenas multiplicativos * /% à esquerda para a associatividade correta.
Pshemo
Bem localizado e alterado em conformidade, obrigado Pshemo
Mark Burleigh
Essa resposta explica a precedência e a associatividade, mas, como Eric Lippert explicou , a questão é sobre a ordem de avaliação, que é muito diferente. Na verdade, isso não responde à pergunta.
Fabio diz Restabelecer Monica de