Efectivamente final vs final - Comportamiento diferente

Sep 04 2020

Hasta ahora pensé que efectivamente final y final son más o menos equivalentes y que el 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 97

Aparentemente, el JLS hace una diferencia importante entre los dos aquí y no estoy seguro de por qué.

Leí otros hilos como

  • Diferencia entre final y efectivamente final
  • Efectivamente variable final vs variable final
  • ¿Qué significa que una variable sea "efectivamente final"?

pero no entran en tales detalles. Después de todo, en un nivel más amplio, parecen ser bastante equivalentes. Pero profundizando, aparentemente difieren.

¿Qué está causando este comportamiento? ¿Alguien puede proporcionar algunas definiciones 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 false

Entonces, la pasantía de cadenas también se comporta de manera diferente aquí (no quiero usar este fragmento en código real, solo tengo curiosidad por el comportamiento diferente).

Respuestas

66 Zabuzard Sep 04 2020 at 16:44

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 finalcampos es muy distinta y está sujeta a grandes optimizaciones del compilador y promesas del modelo de memoria, consulte $ 17.5.1 sobre la semántica de los campos finales.

A nivel superficial finaly effectively finalpara las variables locales son de hecho idénticas. Sin embargo, el JLS hace una clara distinción entre los dos, lo que en realidad tiene una amplia gama de efectos en situaciones especiales como esta.


Premisa

De JLS§4.12.4 sobre finalvariables:

Una variable constante es una finalvariable de tipo primitivo o 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 la clase ( §12.4.1 ), la compatibilidad binaria ( §13.1 ), la accesibilidad ( §14.22 ) y la asignación definida ( §16.1.1 ).

Dado que intes primitiva, la variable aes una variable constante .

Además, del mismo capítulo sobre effectively final:

Ciertas variables que no se declaran finales se consideran en cambio efectivamente finales: ...

Así que desde la forma en que esto está redactado, es claro que en el otro ejemplo, ase no se considera una variable constante, ya que es no es definitivo , pero solamente con eficacia final.


Comportamiento

Ahora que tenemos la distinción, busquemos qué está pasando y por qué la salida es diferente.

Estás usando el operador condicional ? :aquí, por lo que tenemos que verificar su definición. Desde JLS§15.25 :

Hay tres tipos de expresiones condicionales, clasificadas de acuerdo con la segunda y tercera expresiones de operando: expresiones booleanas condicionales , las expresiones condicionales numéricos y expresiones condicionales de referencia .

En este caso, estamos hablando de expresiones condicionales numéricas , de JLS§15.25.2 :

El tipo de 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.

efectivamente final

La versión que effectively finalcorresponde a esta regla:

De lo contrario, la promoción numérica general ( §5.6 ) se aplica al segundo y tercer operandos, y el tipo de expresión condicional es el tipo promovido del segundo y tercer operandos.

Que es el mismo comportamiento que si lo hiciera 5 + 'd', es decir int + char, que resulta en 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 valores del tipo promovido. 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, se aplican a algunas expresiones la conversión de primitivas de ampliación ( §5.1.2 ) y la conversión de primitivas de reducción ( §5.1.3 ), 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 promocionado es int, y otras expresiones que no son de tipo intexperimentan una conversión primitiva amplia a int.

Así que todo se promueve a intque aes un inthecho. Eso explica la salida de 97.

final

La versión con la finalvariable coincide con esta regla:

Si uno de los operandos es de tipo Tdonde Tes byte, shorto char, y el otro operando es una expresión constante ( §15.29 ) de tipo intcuyo valor es representable en tipo T, entonces el tipo de la expresión condicional es T.

La variable final aes de tipo inty expresión constante (porque lo es final). Es representable como char, por lo tanto, el resultado es de tipo char. Con eso concluye la salida a.


Ejemplo de cadena

El ejemplo con la igualdad de cadenas se basa en la misma diferencia central, las finalvariables se tratan como expresión / variable constante y effectively finalno lo es.

En Java, la internación de cadenas se basa en expresiones constantes, por lo tanto

"a" + "b" + "c" == "abc"

es truetambién (no use esta construcción en código real).

Consulte JLS§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, más generalmente , las cadenas que son los valores de expresiones constantes ( §15.29 ) - están "internados" para compartir instancias únicas, utilizando el método String.intern( §12.5 ).

Es fácil de pasar por alto ya que se refiere principalmente a literales, pero en realidad también se aplica a expresiones constantes.

7 DavideLorenzoMARINO Sep 04 2020 at 17:48

Otro aspecto es que si la variable se declara final en el cuerpo del método, tiene un comportamiento diferente al de 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 false

mientras

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 usar final String a = "a"la avariable siempre tendrá el "a"valor para que ay "a"pueda intercambiarse sin problemas. De manera diferente, si ano está definido finalo está definido finalpero su valor se asigna en tiempo de ejecución (como en el ejemplo anterior donde final es el aparámetro), 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 está definida 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 también si su valor es igual a una constante y su valor nunca cambia.