Efectivamente final vs final - Comportamiento diferente

104

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

pero no entran en tantos 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 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 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).

Zabuzard
fuente
2
¡Pregunta muy interesante! Esperaría que Java se comporte de la misma manera en ambos casos, pero ahora estoy iluminado. Me pregunto si este fue siempre el comportamiento o si difiere en versiones anteriores
Lino
8
@Lino El texto de la última cita en la gran respuesta a continuación es el mismo en todo el dorso manera de Java 6 : "Si uno de los operandos es de tipo T , donde T es byte, shorto char, y el otro operando es una expresión constante de tipo intcuyo valor es representable en el tipo T , entonces el tipo de la expresión condicional es T ". --- Incluso encontré un documento de Java 1.0 en Berkeley. Mismo texto . --- Sí, siempre ha sido así.
Andreas
1
La forma en que "encuentras" las cosas es interesante: P De
nada

Respuestas:

65

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, 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í, así 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 en la que 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.

Cuál 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 promovido y, en el caso de una operación aritmética, la operación se define para 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, 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 se intsometen a 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 , cadenas que son los valores de expresiones constantes ( §15.29 ) - están "internados" para compartir instancias únicas, usando 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.

Zabuzard
fuente
8
El problema es que esperaría ... ? a : 'c'comportarse igual si aes una variable o una constante . Aparentemente, no hay nada de malo en la expresión. --- Por el contrario, a + "b" == "ab"es una mala expresión , porque las cadenas deben compararse usando equals()( ¿Cómo comparo cadenas en Java? ). El hecho de que funcione "accidentalmente" cuando aes una constante , es solo una peculiaridad del interior de los literales de cadena.
Andreas
5
@Andreas Sí, pero tenga en cuenta que la pasantía de cadenas es una característica claramente definida de Java. No es una coincidencia que pueda cambiar mañana o en una JVM diferente. "a" + "b" + "c" == "abc"debe estar trueen cualquier implementación Java válida.
Zabuzard
10
Es cierto que es una peculiaridad bien definida, pero a + "b" == "ab"sigue siendo una expresión incorrecta . Incluso si sabe que aes una constante , es demasiado propenso a errores como para no llamar equals(). O tal vez frágil es una palabra mejor, es decir, es muy probable que se desmorone cuando el código se mantenga en el futuro.
Andreas
2
Tenga en cuenta que incluso en el dominio primario de las variables efectivamente finales, es decir, su uso en expresiones lambda, la diferencia puede alterar el comportamiento en tiempo de ejecución, es decir, puede marcar la diferencia entre una expresión lambda capturadora y no capturadora, evaluando esta última a un singleton , pero el primero produce un nuevo objeto. En otras palabras, (final) String str = "a"; Stream.of(null, null). <Runnable>map( x -> () -> System.out.println(str)) .reduce((a,b) -> () -> System.out.println(a == b)) .ifPresent(Runnable::run);cambia su resultado cuando stres (no) final.
Holger
7

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 está definido 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.

Davide Lorenzo MARINO
fuente
4
No hay nada extraño en eso :)
dbl
2
Es otro aspecto de la pregunta.
Davide Lorenzo MARINO
5
finelpalabra clave aplicada a un parámetro, tiene una semántica diferente a la finalaplicada a una variable local, etc ...
dbl
6
El uso del parámetro es una ofuscación innecesaria aquí. Puedes hacer final String a; a = "a";y obtener el mismo comportamiento
yawkat