La variable utilizada en la expresión lambda debe ser final o efectivamente final

134

La variable utilizada en la expresión lambda debe ser final o efectivamente final

Cuando intento usarlo calTz, muestra este error.

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    try {
        cal.getComponents().getComponents("VTIMEZONE").forEach(component -> {
            VTimeZone v = (VTimeZone) component;
            v.getTimeZoneId();
            if (calTz == null) {
                calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
            }
        });
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}
usuario3610470
fuente
55
No se puede modificar calTzdesde la lambda.
Elliott Frisch
2
Asumí que esta era una de esas cosas que simplemente no se hizo a tiempo para Java 8. Pero Java 8 fue 2014. Scala y Kotlin lo han permitido durante años, por lo que obviamente es posible. ¿Alguna vez Java planea eliminar esta extraña restricción?
GlenPeterson
55
Aquí está el enlace actualizado al comentario de @MSDousti.
geisterfurz007
Creo que podría usar Futuros Completables como una solución alternativa.
Kraulain
Una cosa importante que observé: puede usar variables estáticas en lugar de variables normales (esto hace que sea efectivamente final, supongo)
kaushalpranav

Respuestas:

68

Una finalvariable significa que solo se puede instanciar una vez. en Java no puede usar variables no finales en lambda ni en clases internas anónimas.

Puede refactorizar su código con el antiguo ciclo for-each:

private TimeZone extractCalendarTimeZoneComponent(Calendar cal,TimeZone calTz) {
    try {
        for(Component component : cal.getComponents().getComponents("VTIMEZONE")) {
        VTimeZone v = (VTimeZone) component;
           v.getTimeZoneId();
           if(calTz==null) {
               calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
           }
        }
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}

Incluso si no entiendo algunas partes de este código:

  • llamas a un v.getTimeZoneId();sin usar su valor de retorno
  • con la asignación calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());, no modifica el aprobado originalmente calTzy no lo usa en este método
  • Siempre regresas null, ¿por qué no configuras voidcomo tipo de retorno?

Espero que también estos consejos te ayuden a mejorar.

Francesco Pitzalis
fuente
podemos usar variables estáticas no finales
Narendra Jaggi
93

Aunque otras respuestas prueban el requisito, no explican por qué existe el requisito.

El JLS menciona por qué en §15.27.2 :

La restricción a variables finales efectivas prohíbe el acceso a variables locales que cambian dinámicamente, cuya captura probablemente introduciría problemas de concurrencia.

Para reducir el riesgo de errores, decidieron asegurarse de que las variables capturadas nunca se muten.

Dioxina
fuente
11
Buena respuesta +1, y me sorprende la poca cobertura que parece tener el motivo de la final efectiva. Nota: una variable local solo puede ser capturada por una lambda si también se asigna definitivamente antes del cuerpo de la lambda. Ambos requisitos parecen garantizar que el acceso a la variable local sea seguro para subprocesos.
Tim Biegeleisen
2
¿Alguna idea de por qué esto está restringido solo a las variables locales y no a los miembros de la clase? A menudo me encuentro eludiendo el problema al declarar mi variable como miembro de la clase ...
David Refaeli
44
Los miembros de la clase @DavidRefaeli están cubiertos / afectados por el modelo de memoria, que si se sigue, producirá resultados predecibles cuando se compartan. Las variables locales no son, como se menciona en §17.4.1
dioxina
Este es un truco tonto, que debe eliminarse. El compilador debe advertir sobre el posible acceso variable de hilos cruzados, pero debe permitirlo. O bien, debería ser lo suficientemente inteligente como para saber si su lambda se está ejecutando en el mismo hilo, o si se está ejecutando en paralelo, etc. Esta es una limitación tonta, que me pone triste. Y como otros han mencionado, los problemas no existen, por ejemplo, en C #.
Josh M.
@JoshM. C # también le permite crear tipos de valores mutables , que las personas recomiendan evitar para evitar problemas. En lugar de tener tales principios, Java decidió evitarlo por completo. Reduce el error del usuario, al precio de la flexibilidad. No estoy de acuerdo con esta restricción, pero es justificable. Tener en cuenta el paralelismo requeriría un poco de trabajo adicional en el extremo del compilador, lo que probablemente es la razón por la cual no se tomó la ruta de " advertir acceso cruzado ". Un desarrollador que trabaje en la especificación probablemente sea nuestra única confirmación para esto.
Dioxina
58

De una lambda, no puedes obtener una referencia a nada que no sea definitivo. Debe declarar un contenedor final desde fuera del lamda para mantener su variable.

He agregado el objeto de 'referencia' final como este contenedor.

private TimeZone extractCalendarTimeZoneComponent(Calendar cal,TimeZone calTz) {
    final AtomicReference<TimeZone> reference = new AtomicReference<>();

    try {
       cal.getComponents().getComponents("VTIMEZONE").forEach(component->{
        VTimeZone v = (VTimeZone) component;
           v.getTimeZoneId();
           if(reference.get()==null) {
               reference.set(TimeZone.getTimeZone(v.getTimeZoneId().getValue()));
           }
           });
    } catch (Exception e) {
        //log.warn("Unable to determine ical timezone", e);
    }
    return reference.get();
}   
DMozzy
fuente
Estaba pensando en el mismo enfoque o en uno similar, pero ¿me gustaría ver algún consejo / comentario experto sobre esta respuesta?
YoYo
44
Este código pierde una inicial reference.set(calTz);o la referencia debe crearse utilizando new AtomicReference<>(calTz), de lo contrario, se perderá la TimeZone no nula proporcionada como parámetro.
Julien Kronegg
8
Esta debería ser la primera respuesta. Una AtomicReference (o una clase Atomic___ similar) evita esta limitación de forma segura en todas las circunstancias posibles.
GlenPeterson
1
De acuerdo, esta debería ser la respuesta aceptada. Las otras respuestas brindan información útil sobre cómo recurrir a un modelo de programación no funcional, y sobre por qué se hizo esto, ¡pero en realidad no le dicen cómo solucionar el problema!
Jonathan Benn
2
@GlenPeterson y también es una decisión terrible, no solo es mucho más lento de esta manera, sino que también ignora la propiedad de efectos secundarios que exige la documentación.
Eugene
41

Java 8 tiene un nuevo concepto llamado variable "Efectivamente final". Significa que una variable local no final cuyo valor nunca cambia después de la inicialización se llama "Efectivamente Final".

Este concepto se introdujo porque antes de Java 8 , no podíamos usar una variable local no final en una clase anónima . Si desea tener acceso a una variable local en clase anónima , debe hacerlo final.

Cuando se introdujo lambda, esta restricción se alivió. Por lo tanto, la necesidad de hacer que la variable local sea final si no se cambia una vez que se inicializa como lambda en sí misma no es más que una clase anónima.

Java 8 se dio cuenta de la molestia de declarar la variable local final cada vez que un desarrollador usaba lambda, introdujo este concepto e hizo innecesario hacer que las variables locales fueran finales. Entonces, si ve que la regla para las clases anónimas no ha cambiado, es solo que no tiene que escribir la finalpalabra clave cada vez que usa lambdas.

Encontré una buena explicación aquí

Dinesh Arora
fuente
El formato de código solo debe usarse para código , no para términos técnicos en general. effectively finalNo es código, es terminología. Consulte ¿ Cuándo se debe usar el formato de código para texto sin código? en Meta Stack Overflow .
Charles Duffy
(Entonces, "la finalpalabra clave" es una palabra de código y correcta para formatear de esa manera, pero cuando usa "final" descriptivamente en lugar de como código, es la terminología en su lugar).
Charles Duffy
9

En su ejemplo, puede reemplazar el forEachcon lamdba con un forbucle simple y modificar cualquier variable libremente. O, probablemente, refactorice su código para que no necesite modificar ninguna variable. Sin embargo, explicaré por completo qué significa el error y cómo solucionarlo.

Especificación del lenguaje Java 8, §15.27.2 :

Cualquier variable local, parámetro formal o parámetro de excepción utilizado pero no declarado en una expresión lambda debe declararse final o efectivamente final ( §4.12.4 ), o se produce un error en tiempo de compilación cuando se intenta el uso.

Básicamente no puede modificar una variable local ( calTzen este caso) desde una lambda (o una clase local / anónima). Para lograr eso en Java, debe usar un objeto mutable y modificarlo (a través de una variable final) desde la lambda. Un ejemplo de un objeto mutable aquí sería una matriz de un elemento:

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    TimeZone[] result = { null };
    try {
        cal.getComponents().getComponents("VTIMEZONE").forEach(component -> {
            ...
            result[0] = ...;
            ...
        }
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return result[0];
}
Alexander Udalov
fuente
Otra forma es usar un campo de un objeto. Por ejemplo, MyObj result = new MyObj (); ... result.timeZone = ...; ....; return result.timezone; Sin embargo, tenga en cuenta que, como se explicó anteriormente, esto lo expone a problemas de seguridad de subprocesos. Ver stackoverflow.com/a/50341404/7092558
Gibezynu Nu
0

Si no es necesario modificar la variable, una solución general para este tipo de problema sería extraer la parte del código que utiliza lambda y la palabra clave final en el método-parámetro.

robie2011
fuente
0

Una variable utilizada en la expresión lambda debe ser una final o efectivamente final, pero puede asignar un valor a una matriz de un elemento final.

private TimeZone extractCalendarTimeZoneComponent(Calendar cal, TimeZone calTz) {
    try {
        TimeZone calTzLocal[] = new TimeZone[1];
        calTzLocal[0] = calTz;
        cal.getComponents().get("VTIMEZONE").forEach(component -> {
            TimeZone v = component;
            v.getTimeZoneId();
            if (calTzLocal[0] == null) {
                calTzLocal[0] = TimeZone.getTimeZone(v.getTimeZoneId().getValue());
            }
        });
    } catch (Exception e) {
        log.warn("Unable to determine ical timezone", e);
    }
    return null;
}
Andreas Foteas
fuente
Esto es muy similar a la sugerencia de Alexander Udalov. Aparte de eso, creo que este enfoque se basa en los efectos secundarios.
Scratte