Hasta ahora, pensé que efectivamente final y final son más o menos equivalentes y que JLS los trataría de manera similar, si no idéntica, en el comportamiento real. Entonces encontré este escenario artificial:
final int a = 97; System.out.println(true ? a : 'c'); // outputs a // versus int a = 97; System.out.println(true ? a : 'c'); // outputs 97Aparentemente, el JLS hace una diferencia importante entre los dos aquí y no estoy seguro de por qué.
Leí otros hilos como
pero no entran en tantos detalles. Después de todo, en un nivel más amplio parecen ser bastante equivalentes. Pero profundizando más, aparentemente difieren.
¿Qué está causando este comportamiento? ¿Alguien puede proporcionar algunas definiciones de JLS que expliquen esto?
Editar: encontré otro escenario relacionado:
final String a = "a"; System.out.println(a + "b" == "ab"); // outputs true // versus String a = "a"; System.out.println(a + "b" == "ab"); // outputs falseEntonces, la cadena interna también se comporta de manera diferente aquí (no quiero usar este fragmento en código real, solo tengo curiosidad por el comportamiento diferente).
En primer lugar, estamos hablando solo de variables locales . Efectivamente final no se aplica a los campos. Esto es importante, ya que la semántica de los campos final es muy distinta y está sujeta a fuertes optimizaciones del compilador y promesas del modelo de memoria, consulte $17.5.1 sobre la semántica de los campos finales.
En un nivel superficial, final y effectively final para las variables locales son, de hecho, idénticos. Sin embargo, el JLS hace una clara distinción entre los dos que en realidad tiene una amplia gama de efectos en situaciones especiales como esta.
DeJLS§4.12.4 sobre variables final :
Una variable constante es una variable
finalde tipo primitivo o de tipo String que se inicializa con una expresión constante (§15.29 ). Si una variable es una variable constante o no , puede tener implicaciones con respecto a la inicialización de clase ( §12.4.1 ), la compatibilidad binaria ( §13.1 ), la accesibilidad (§14.22 ) y la asignación definitiva ( §16.1.1 ).
Dado que int es primitivo, la variable a es una variable constante .
Además, del mismo capítulo sobre effectively final :
Ciertas variables que no se declaran finales se consideran efectivamente finales: ...
Entonces, por la forma en que está redactado, está claro que en el otro ejemplo, a no se considera una variable constante, ya que no es final , sino solo efectivamente final.
Ahora que tenemos la distinción, busquemos qué está pasando y por qué la salida es diferente.
¿Está utilizando el operador condicional ? : aquí, así que tenemos que comprobar su definición. DeJLS§15.25 :
Hay tres tipos de expresiones condicionales, clasificadas según el segundo y el tercer operando: expresiones condicionales booleanas, expresiones condicionales numéricas y expresiones condicionales de referencia .
En este caso, estamos hablando de expresiones condicionales numéricas , de JLS§15.25.2 :
El tipo de una expresión condicional numérica se determina de la siguiente manera:
Y esa es la parte donde los dos casos se clasifican de manera diferente.
La versión que es effectively final se corresponde con esta regla:
De lo contrario, la promoción numérica general ( §5.6 ) se aplica a los operandos segundo y tercero, y el tipo de la expresión condicional es el tipo promovido de los operandos segundo y tercero.
Que es el mismo comportamiento que si hiciera 5 + 'd' , es decir, int + char , lo que da como resultado int . Ver JLS§5.6
La promoción numérica determina el tipo promocionado de todas las expresiones en un contexto numérico. El tipo promocionado se elige de modo que cada expresión se pueda convertir al tipo promocionado y, en el caso de una operación aritmética, la operación se define para los valores del tipo promocionado. El orden de las expresiones en un contexto numérico no es significativo para la promoción numérica. Las reglas son las siguientes:
[...]
A continuación, la conversión de primitivas de ampliación ( §5.1.2 ) y la conversión de primitivas de estrechamiento ( §5.1.3 ) se aplican a algunas expresiones, de acuerdo con las siguientes reglas:
En un contexto de elección numérica, se aplican las siguientes reglas:
Si alguna expresión es de tipo
inty no es una expresión constante (§15.29 ), entonces el tipo promovido esint, y otras expresiones que no son de tipointexperimentan una conversión primitiva ampliada aint.
Así que todo se promociona a int como ya es a int . Eso explica la salida de 97 .
La versión con la variable final coincide con esta regla:
Si uno de los operandos es de tipo
TdondeTesbyte,shortochar, y el otro operando es una expresión constante (§15.29 ) de tipointcuyo valor es representable en tipoT, entonces el tipo de la expresión condicional esT
La variable final a es de tipo int y una expresión constante (porque es final ). Se puede representar como char , por lo que el resultado es de tipo char . Eso concluye la salida a .
El ejemplo con la igualdad de cadenas se basa en la misma diferencia central, las variables final se tratan como expresión/variable constante y, effectively final no lo es.
En Java, la internación de cadenas se basa en expresiones constantes, por lo tanto
"a" + "b" + "c" == "abc" también es true (no use esta construcción en código real).
VerJLS§3.10.5 :
Además, un literal de cadena siempre se refiere a la misma instancia de la clase String. Esto se debe a que los literales de cadena, o, de manera más general , las cadenas que son los valores de las expresiones constantes (§15.29 ), se "internan" para compartir instancias únicas, utilizando el método
String.intern( §12.5 ).
Fácil de pasar por alto, ya que se trata principalmente de literales, pero en realidad también se aplica a expresiones constantes.
Otro aspecto es que si la variable se declara final en el cuerpo del método tiene un comportamiento diferente a una variable final pasada como parámetro.
public void testFinalParameters(final String a, final String b) { System.out.println(a + b == "ab"); } ... testFinalParameters("a", "b"); // Prints falsemientras
public void testFinalVariable() { final String a = "a"; final String b = "b"; System.out.println(a + b == "ab"); // Prints true } ... testFinalVariable(); sucede porque el compilador sabe que usando final String a = "a" la variable a siempre tendrá el valor "a" para que a y "a" se puedan intercambiar sin problemas. De manera diferente, si a no se define como final o si se define como final pero su valor se asigna en tiempo de ejecución (como en el ejemplo anterior donde final es el parámetro a ), el compilador no sabe nada antes de su uso. Entonces, la concatenación ocurre en tiempo de ejecución y se genera una nueva cadena, sin usar el grupo interno.
Básicamente, el comportamiento es: si el compilador sabe que una variable es una constante, puede usarla de la misma manera que usa la constante.
Si la variable no se define como final (o es final pero su valor se define en tiempo de ejecución), no hay razón para que el compilador la maneje como una constante, incluso si su valor es igual a una constante y su valor nunca cambia.