Primeiro, um quebra-cabeça: o que o código a seguir imprime?
public class RecursiveStatic {
public static void main(String[] args) {
System.out.println(scale(5));
}
private static final long X = scale(10);
private static long scale(long value) {
return X * value;
}
}
Responda:
0 0
Spoilers abaixo.
Se você imprimir X
em escala (longa) e redefinir X = scale(10) + 3
, as impressões serão X = 0
então X = 3
. Isso significa que X
está temporariamente definido como 0
e posteriormente definido como 3
. Isso é uma violação de final
!
O modificador estático, em combinação com o modificador final, também é usado para definir constantes. O modificador final indica que o valor desse campo não pode ser alterado .
Fonte: https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [ênfase adicionada]
Minha pergunta: isso é um bug? Está final
mal definido?
Aqui está o código no qual estou interessado.
X
Recebe dois valores diferentes: 0
e 3
. Eu acredito que isso seja uma violação de final
.
public class RecursiveStatic {
public static void main(String[] args) {
System.out.println(scale(5));
}
private static final long X = scale(10) + 3;
private static long scale(long value) {
System.out.println("X = " + X);
return X * value;
}
}
Esta pergunta foi sinalizada como uma possível duplicata da ordem de inicialização do campo final estático de Java . Acredito que esta questão não seja uma duplicata, pois a outra pergunta aborda a ordem de inicialização, enquanto minha pergunta aborda uma inicialização cíclica combinada com a final
tag. Apenas a partir da outra pergunta, não seria possível entender por que o código na minha pergunta não comete um erro.
Isso é especialmente claro ao observar a saída que o ernesto obtém: quando a
é marcado com final
, ele obtém a seguinte saída:
a=5
a=5
que não envolve a parte principal da minha pergunta: como uma final
variável altera sua variável?
fonte
X
membro é como se referir a um membro da subclasse antes que o construtor da superclasse termine, esse é seu problema e não a definição definal
.A blank final instance variable must be definitely assigned (§16.9) at the end of every constructor (§8.8) of the class in which it is declared; otherwise a compile-time error occurs.
Respostas:
Um achado muito interessante. Para entendê-lo, precisamos procurar na Java Language Specification ( JLS ).
O motivo é que
final
apenas permite uma tarefa . O valor padrão, no entanto, não é uma atribuição . De fato, todas essas variáveis ( variável de classe, variável de instância, componente de matriz) apontam para seu valor padrão desde o início, antes das atribuições . A primeira atribuição altera a referência.Variáveis de classe e valor padrão
Veja o seguinte exemplo:
Não atribuímos explicitamente um valor
x
, embora aponte paranull
, seu valor padrão. Compare isso com §4.12.5 :Observe que isso vale apenas para esse tipo de variáveis, como no nosso exemplo. Não é válido para variáveis locais, consulte o seguinte exemplo:
Do mesmo parágrafo JLS:
Variáveis finais
Agora vamos dar uma olhada
final
, de §4.12.4 :Explicação
Agora voltando ao seu exemplo, ligeiramente modificado:
Produz
Lembre-se do que aprendemos. Dentro do método,
assign
a variávelX
ainda não foi atribuída a um valor. Portanto, aponta para seu valor padrão, pois é uma variável de classe e, de acordo com o JLS, essas variáveis sempre apontam imediatamente para seus valores padrão (em contraste com as variáveis locais). Após oassign
método, a variávelX
recebe o valor1
e, porfinal
isso, não podemos mais alterá-lo. Portanto, o seguinte não funcionaria devido afinal
:Exemplo no JLS
Graças a @ Andrew, eu encontrei um parágrafo JLS que cobre exatamente esse cenário, ele também o demonstra.
Mas primeiro vamos dar uma olhada
Por que isso não é permitido, enquanto o acesso do método é? Dê uma olhada no §8.3.3, que fala sobre quando o acesso aos campos é restrito se o campo ainda não foi inicializado.
Ele lista algumas regras relevantes para variáveis de classe:
É simples, o
X = X + 1
é pego por essas regras, o método não acessa. Eles até listam esse cenário e dão um exemplo:fonte
X
partir do método. Eu não me importaria muito. Depende apenas de como exatamente o JLS define as coisas para trabalhar em detalhes. Eu nunca usaria código assim, apenas explorando algumas regras no JLS.forwards references
(que também faz parte do JLS). Isto é tão simples sem este looong resposta stackoverflow.com/a/49371279/1059372Nada a ver com final aqui.
Como está no nível da instância ou da classe, ele mantém o valor padrão se nada for atribuído ainda. Essa é a razão que você vê
0
ao acessá-lo sem atribuir.Se você acessar
X
sem atribuir completamente, ele manterá os valores padrão de long, ou seja0
, daí os resultados.fonte
Não é um bug.
Quando a primeira chamada para
scale
é chamada deEle tenta avaliar
return X * value
.X
ainda não recebeu um valor e, portanto, o valor padrão para along
é usado (o que é0
).Portanto, essa linha de código é avaliada como
X * 10
ie0 * 10
qual é0
.fonte
X = scale(10) + 3
. DesdeX
, quando referenciada a partir do método, é0
. Mas depois é3
. Portanto, o OP pensa queX
são atribuídos dois valores diferentes, com os quais entrariam em conflitofinal
.return X * value
.X
Não foi atribuído um valor ainda e, portanto, assume o valor padrão para umlong
que é0
. "? Não se diz queX
é atribuído com o valor padrão, mas que eleX
é "substituído" (não cite esse termo;)) pelo valor padrão.Não é um bug, simplesmente, não é uma forma ilegal de referência direta, nada mais.
É simplesmente permitido pela Especificação.
Para dar o seu exemplo, é exatamente aqui que isso corresponde:
Você está fazendo uma referência direta a
scale
isso que não é ilegal, como dito anteriormente, mas permite que você obtenha o valor padrão deX
. Novamente, isso é permitido pelo Spec (para ser mais exato, não é proibido), portanto funciona muito bemfonte
Os membros de nível de classe podem ser inicializados no código dentro da definição de classe. O bytecode compilado não pode inicializar os membros da classe em linha. (Os membros da instância são tratados da mesma forma, mas isso não é relevante para a pergunta fornecida.)
Quando alguém escreve algo como o seguinte:
O bytecode gerado seria semelhante ao seguinte:
O código de inicialização é colocado em um inicializador estático, que é executado quando o carregador de classes carrega a classe pela primeira vez. Com esse conhecimento, sua amostra original seria semelhante à seguinte:
scale(10)
para atribuir ostatic final
campoX
.scale(long)
função é executada enquanto a classe é parcialmente inicializada, lendo o valor não inicializadoX
cujo padrão é long ou 0.0 * 10
é atribuídoX
e o carregador de classes é concluído.scale(5)
que multiplica 5 peloX
valor agora inicializado de 0 retornando 0.O campo final estático
X
é atribuído apenas uma vez, preservando a garantia mantida pelafinal
palavra - chave. Para a consulta subsequente da adição de 3 na atribuição, a etapa 5 acima torna-se a avaliação de0 * 10 + 3
qual é o valor3
e o método principal imprimirá o resultado desse3 * 5
valor15
.fonte
A leitura de um campo não inicializado de um objeto deve resultar em um erro de compilação. Infelizmente para Java, isso não acontece.
Eu acho que a razão fundamental pela qual esse é o caso está "escondida" profundamente na definição de como os objetos são instanciados e construídos, embora eu não conheça os detalhes do padrão.
Em certo sentido, final é mal definido, porque nem mesmo cumpre o que seu objetivo declarado é devido a esse problema. No entanto, se todas as suas aulas forem escritas corretamente, você não terá esse problema. Significando que todos os campos são sempre definidos em todos os construtores e nenhum objeto é criado sem chamar um de seus construtores. Isso parece natural até que você precise usar uma biblioteca de serialização.
fonte