Por que esse código Java é compilado?

96

No método ou no escopo da classe, a linha abaixo é compilada (com aviso):

int x = x = 1;

No escopo da classe, onde as variáveis ​​obtêm seus valores padrão , o seguinte fornece o erro de 'referência indefinida':

int x = x + 1;

Não é o primeiro x = x = 1deve terminar com o mesmo erro de 'referência indefinida'? Ou talvez a segunda linha int x = x + 1deva compilar? Ou há algo que estou perdendo?

Marcin
fonte
1
Se você adicionar a palavra-chave staticna variável de escopo de classe, como em static int x = x + 1;, obterá o mesmo erro? Porque em C # faz diferença se é estático ou não estático.
Jeppe Stig Nielsen
static int x = x + 1falha em Java.
Marcin,
1
no c # int a = this.a + 1;e int b = 1; int a = b + 1;no escopo da classe (ambos ok em Java) falham, provavelmente devido a §17.4.5.2 - "Um inicializador de variável para um campo de instância não pode fazer referência à instância que está sendo criada." Não sei se é explicitamente permitido em algum lugar, mas estático não tem essa restrição. Em Java as regras são diferentes e static int x = x + 1não pela mesma razão que int x = x + 1faz
MSAM
Essa resposta com um bytecode esclarece todas as dúvidas.
rgripper de

Respostas:

101

tl; dr

Para campos , int b = b + 1é ilegal porque bé uma referência de encaminhamento ilegal para b. Você pode realmente consertar isso escrevendo int b = this.b + 1, que compila sem reclamações.

Para variáveis ​​locais , int d = d + 1é ilegal porque dnão é inicializado antes do uso. Este não é o caso dos campos, que são sempre inicializados por padrão.

Você pode ver a diferença ao tentar compilar

int x = (x = 1) + x;

como uma declaração de campo e como uma declaração de variável local. O primeiro falhará, mas o último terá sucesso, por causa da diferença na semântica.

Introdução

Em primeiro lugar, as regras para inicializadores de campo e variável local são muito diferentes. Portanto, esta resposta abordará as regras em duas partes.

Usaremos este programa de teste em:

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;
    }
}

A declaração de bé inválida e falha com um illegal forward referenceerro.
A declaração de dé inválida e falha com um variable d might not have been initializederro.

O fato de esses erros serem diferentes deve indicar que as razões para os erros também são diferentes.

Campos

Os inicializadores de campo em Java são governados por JLS §8.3.2 , Inicialização de campos.

O escopo de um campo é definido em JLS §6.3 , Escopo de uma declaração.

As regras relevantes são:

  • O escopo de uma declaração de um membro mdeclarado em ou herdado por um tipo de classe C (§8.1.6) é o corpo inteiro de C, incluindo quaisquer declarações de tipo aninhado.
  • As expressões de inicialização para variáveis ​​de instância podem usar o nome simples de qualquer variável estática declarada na classe ou herdada pela classe, mesmo uma cuja declaração ocorra textualmente mais tarde.
  • O uso de variáveis ​​de instância cujas declarações aparecem textualmente após o uso às vezes é restrito, mesmo que essas variáveis ​​de instância estejam no escopo. Consulte §8.3.2.3 para as regras precisas que regem a referência direta a variáveis ​​de instância.

§8.3.2.3 diz:

A declaração de um membro precisa aparecer textualmente antes de ser usada apenas se o membro for um campo de instância (respectivamente estático) de uma classe ou interface C e todas as seguintes condições forem válidas:

  • O uso ocorre em um inicializador de variável de instância (respectivamente estático) de C ou em um inicializador de instância (respectivamente estático) de C.
  • O uso não está no lado esquerdo de uma atribuição.
  • O uso é feito por meio de um nome simples.
  • C é a classe ou interface mais interna que envolve o uso.

Na verdade, você pode consultar os campos antes de serem declarados, exceto em certos casos. Essas restrições têm como objetivo evitar códigos como

int j = i;
int i = j;

da compilação. A especificação Java diz que "as restrições acima são projetadas para capturar, em tempo de compilação, inicializações circulares ou malformadas".

O que essas regras realmente se resumem?

Em suma, as regras basicamente dizem que você deve declarar um campo antes de uma referência a esse campo se (a) a referência estiver em um inicializador, (b) a referência não estiver sendo atribuída, (c) a referência for um nome simples (sem qualificadores como this.) e (d) não está sendo acessado de dentro de uma classe interna. Portanto, uma referência direta que satisfaça todas as quatro condições é ilegal, mas uma referência direta que falhe em pelo menos uma condição está OK.

int a = a = 1;compila porque viola (b): a referência a está sendo atribuída, portanto, é legal referir-se aantes da adeclaração completa de.

int b = this.b + 1também compila porque viola (c): a referência this.bnão é um nome simples (é qualificada com this.). Essa estranha construção ainda está perfeitamente bem definida, pois this.btem o valor zero.

Portanto, basicamente, as restrições nas referências de campo nos inicializadores evitam que int a = a + 1sejam compilados com sucesso.

Observe que a declaração de campo int b = (b = 1) + birá falhar para compilar, porque a final bainda é uma referência para a frente ilegal.

Variáveis ​​locais

As declarações de variáveis ​​locais são regidas por JLS §14.4 , Declarações de declaração de variáveis ​​locais.

O escopo de uma variável local é definido em JLS §6.3 , Escopo de uma declaração:

  • O escopo de uma declaração de variável local em um bloco (§14.4) é o resto do bloco no qual a declaração aparece, começando com seu próprio inicializador e incluindo quaisquer declaradores adicionais à direita na declaração de declaração de variável local.

Observe que os inicializadores estão dentro do escopo da variável sendo declarada. Então, por que não int d = d + 1;compila?

O motivo é devido à regra de Java sobre atribuição definitiva ( JLS §16 ). A atribuição definida basicamente diz que todo acesso a uma variável local deve ter uma atribuição anterior a essa variável, e o compilador Java verifica os loops e ramificações para garantir que a atribuição sempre ocorra antes de qualquer uso (é por isso que a atribuição definida tem uma seção de especificação inteira dedicada para ele). A regra básica é:

  • Para cada acesso de uma variável local ou campo final em branco x, xdeve ser definitivamente atribuído antes do acesso, ou ocorrerá um erro em tempo de compilação.

Em int d = d + 1;, o acesso a dé resolvido para a variável local fine, mas como dnão foi atribuído antes de dser acessado, o compilador emite um erro. Em int c = c = 1, c = 1acontece primeiro, que atribui ce, em seguida, cé inicializado com o resultado dessa atribuição (que é 1).

Observe que por causa das regras de atribuição definidas, a declaração da variável local int d = (d = 1) + d; será compilada com sucesso ( ao contrário da declaração do campo int b = (b = 1) + b), porque dé definitivamente atribuída no momento em que o final dé alcançado.

Nneonneo
fonte
+1 para as referências, no entanto, acho que você errou na formulação: "int a = a = 1; compila porque viola (b)", se violasse qualquer um dos 4 requisitos não compilaria. No entanto isso não acontecer, uma vez que é no lado esquerdo de uma atribuição (duplo negativo na redação do JLS não ajuda muito aqui). Em int b = b + 1b está à direita (não à esquerda) da atribuição, portanto, violaria isso ...
msam
... O que não tenho certeza é o seguinte: essas 4 condições devem ser atendidas se a declaração não aparecer textualmente antes da cessão, neste caso eu acho que a declaração aparece "textualmente" antes da cessão int x = x = 1, na qual caso nada disso se aplicaria.
msam
@msam: É um pouco confuso, mas basicamente você tem que violar uma das quatro condições para fazer uma referência direta. Se sua referência de encaminhamento satisfizer todas as quatro condições, isso é ilegal.
nneonneo,
@msam: Além disso, a declaração completa só tem efeito após o inicializador.
nneonneo,
@mrfishie: Grande resposta, mas há uma quantidade surpreendente de profundidade nas especificações Java. A questão não é tão simples quanto parece superficialmente. (Eu escrevi um subconjunto de compilador Java uma vez, então estou bastante familiarizado com muitos dos prós e contras do JLS).
nneonneo,
86
int x = x = 1;

é equivalente a

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

enquanto em

int x = x + 1; 

primeiro precisamos calcular, x+1mas o valor de x não é conhecido, então você obtém um erro (o compilador sabe que o valor de x não é conhecido)

msam
fonte
4
Isso mais a dica sobre a associatividade correta do OpenSauce que achei muito útil.
TobiMcNamobi
1
Achei que o valor de retorno de uma atribuição fosse o valor atribuído, não o valor da variável.
zzzzBov
2
@zzzzBov está correto. int x = x = 1;é equivalente a int x = (x = 1), não x = 1; x = x; . Você não deve receber um aviso do compilador por fazer isso.
nneonneo
int x = x = 1;s equivalente a int x = (x = 1)por causa da associatividade à direita do =operador
Grijesh Chauhan,
1
@nneonneo e int x = (x = 1)é equivalente a int x; x = 1; x = x;(declaração da variável, avaliação do inicializador de campo, atribuição da variável ao resultado da referida avaliação), daí o aviso
msam
41

É aproximadamente equivalente a:

int x;
x = 1;
x = 1;

Em primeiro lugar, int <var> = <expression>;é sempre equivalente a

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

Nesse caso, sua expressão é x = 1, que também é uma afirmação. x = 1é uma declaração válida, uma vez que o var xjá foi declarado. É também uma expressão com o valor 1, ao qual é atribuído xnovamente.

OpenSauce
fonte
Ok, mas se foi como você disse, por que no escopo da classe a segunda instrução dá um erro? Quer dizer, você obtém o 0valor padrão para ints, então eu esperaria que o resultado fosse 1, não o undefined reference.
Marcin
Dê uma olhada na resposta @izogfif. Parece funcionar, porque o compilador C ++ atribui valores padrão às variáveis. Da mesma forma que o java faz para variáveis ​​de nível de classe.
Marcin,
@Marcin: em Java, os ints não são inicializados com 0 quando são variáveis ​​locais. Eles só são inicializados com 0 se forem variáveis ​​de membro. Portanto, em sua segunda linha, x + 1não tem valor definido, pois xnão foi inicializado.
OpenSauce
1
@OpenSauce Mas x é definido como uma variável de membro ("no escopo da classe").
Jacob Raihle
@JacobRaihle: Ah ok, não percebi essa parte. Não tenho certeza se o bytecode para inicializar uma var para 0 será gerado pelo compilador se ele vir que há uma instrução de inicialização explícita. Há um artigo aqui que dá alguns detalhes sobre a inicialização de classes e objetos, embora eu não ache que aborde exatamente esse problema: javaworld.com/jw-11-2001/jw-1102-java101.html
OpenSauce
12

Em java ou em qualquer linguagem moderna, a atribuição vem da direita.

Suponha que você tenha duas variáveis ​​x e y,

int z = x = y = 5;

Esta declaração é válida e é assim que o compilador os divide.

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

Mas no seu caso

int x = x + 1;

O compilador deu uma exceção porque, ele se divide assim.

x = 1; // oops, it isn't declared because assignment comes from the right.
Sri Harsha Chilakapati
fonte
o aviso está ativado x = x não x = 1
Asim Ghaffar,
8

int x = x = 1; não é igual a:

int x;
x = 1;
x = x;

javap nos ajuda novamente, estas são instruções JVM geradas para este código:

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

mais como:

int x = 1;
x = 1;

Não há razão para lançar um erro de referência indefinida. Agora há uso de variável antes de sua inicialização, portanto, este código está em total conformidade com a especificação. Na verdade, não há nenhum uso de variável , apenas atribuições. E o compilador JIT irá ainda mais longe, eliminará tais construções. Sinceramente, não entendo como esse código está conectado à especificação JLS de inicialização e uso de variáveis. Sem uso, sem problemas. ;)

Por favor, corrija se eu estiver errado. Não consigo entender por que outras respostas, que se referem a muitos parágrafos do JLS, reúnem tantos pontos positivos. Esses parágrafos não têm nada em comum com este caso. Apenas duas atribuições em série e nada mais.

Se escrevermos:

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

é igual a:

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

A expressão mais à direita é apenas atribuída a variáveis ​​uma a uma, sem qualquer recursão. Podemos bagunçar as variáveis ​​da maneira que quisermos:

a = b = c = f = e = d = a = a = a = a = a = e = f = 5;
Mikhail
fonte
7

Em int x = x + 1;você adiciona 1 ax, então qual é o valor de x, ainda não foi criado.

Mas o in int x=x=1;irá compilar sem nenhum erro porque você atribui 1 a x.

Alya'a Gamal
fonte
5

Seu primeiro trecho de código contém um segundo em =vez de um sinal de adição. Isso será compilado em qualquer lugar, enquanto a segunda parte do código não será compilada em nenhum deles.

Joe Elleson
fonte
5

Na segunda parte do código, x é usado antes de sua declaração, enquanto na primeira parte do código ele é apenas atribuído duas vezes, o que não faz sentido, mas é válido.

WilQu
fonte
5

Vamos decompô-lo passo a passo, associativo correto

int x = x = 1

x = 1, atribua 1 a uma variável x

int x = x, atribua o que x é para si mesmo, como um int. Como x foi atribuído anteriormente como 1, ele retém 1, embora de maneira redundante.

Isso compila bem.

int x = x + 1

x + 1, adicione um a uma variável x. No entanto, sendo x indefinido, ocorrerá um erro de compilação.

int x = x + 1, portanto, esta linha compila erros como a parte direita de igual não compilará adicionando um a uma variável não atribuída

Steventnorris
fonte
Não, é associativo à direita quando há dois =operadores, então é o mesmo que int x = (x = 1);.
Jeppe Stig Nielsen
Ah, minhas ordens fora. Me desculpe por isso. Deveria ter feito isso ao contrário. Eu mudei agora.
steventnorris
3

O segundo int x=x=1é compilar porque você está atribuindo o valor ax, mas em outro caso int x=x+1aqui a variável x não é inicializada. Lembre-se de que as variáveis ​​locais java não são inicializadas com o valor padrão. Nota Se for ( int x=x+1) também estiver no escopo da classe, haverá um erro de compilação já que a variável não foi criada.

Krushna
fonte
2
int x = x + 1;

compila com sucesso no Visual Studio 2008 com aviso

warning C4700: uninitialized local variable 'x' used`
izogfif
fonte
2
Interresting. É C / C ++?
Marcin
@Marcin: sim, é C ++. @msam: desculpe, acho que vi tag em cvez de, javamas aparentemente era a outra pergunta.
izogfif
Ele compila porque em C ++, os compiladores atribuem valores padrão para tipos primitivos. Use bool y;e y==trueretornará false.
Sri Harsha Chilakapati
@SriHarshaChilakapati, é algum tipo de padrão no compilador C ++? Porque quando eu compilo void main() { int x = x + 1; printf("%d ", x); }no Visual Studio 2008, em Debug eu obtenho a exceção Run-Time Check Failure #3 - The variable 'x' is being used without being initialized.e em Release eu recebo o número 1896199921impresso no console.
izogfif
1
@SriHarshaChilakapati Falando sobre outras linguagens: Em C #, para um staticcampo (variável estática de nível de classe), as mesmas regras se aplicam. Por exemplo, um campo declarado como public static int x = x + 1;compila sem aviso no Visual C #. Possivelmente o mesmo em Java?
Jeppe Stig Nielsen
2

x não é inicializado em x = x + 1;.

A linguagem de programação Java é tipificada estaticamente, o que significa que todas as variáveis ​​devem primeiro ser declaradas antes de serem usadas.

Veja os tipos de dados primitivos

Mohan Raj B
fonte
3
A necessidade de inicializar variáveis ​​antes de usar seus valores não tem nada a ver com tipagem estática. Com tipo estático: você precisa declarar o tipo de variável. Inicializar antes de usar: ele precisa ter um valor comprovadamente antes de você poder usar o valor.
Jon Bright
@JonBright: A necessidade de declarar tipos de variáveis ​​também não tem nada a ver com tipagem estática. Por exemplo, existem linguagens tipadas estaticamente com inferência de tipo.
hammar
@hammar, a meu ver, você pode argumentar de duas maneiras: com a inferência de tipo, você está declarando implicitamente o tipo da variável de uma forma que o sistema possa inferir. Ou, a inferência de tipo é uma terceira forma, onde as variáveis ​​não são dinamicamente digitadas em tempo de execução, mas estão no nível de origem, dependendo de seu uso e das inferências feitas. De qualquer maneira, a afirmação permanece verdadeira. Mas você está certo, eu não estava pensando em outros sistemas de tipo.
Jon Bright
2

A linha de código não é compilada com um aviso devido ao modo como o código realmente funciona. Quando você executa o código int x = x = 1, Java primeiro cria a variável x, conforme definido. Em seguida , executa o código de atribuição ( x = 1). Como xjá está definido, o sistema não tem erros configurados xpara 1. Isso retorna o valor 1, porque agora é o valor de x. Portanto, xagora está finalmente definido como 1.
Java basicamente executa o código como se fosse este:

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

No entanto, em sua segunda parte do código, int x = x + 1a + 1instrução precisa xser definida, o que até então não é. Como as instruções de atribuição sempre significam que o código à direita do =é executado primeiro, o código falhará porque xé indefinido. Java executaria o código assim:

int x;
x = x + 1; // this line causes the error because `x` is undefined
cpdt
fonte
-1

O compilador leu as declarações da direita para a esquerda e planejamos fazer o oposto. É por isso que me incomodou no início. Faça disso um hábito de ler declarações (código) da direita para a esquerda, você não terá esse problema.

Ramiz Uddin
fonte