La variable utilisée dans l'expression lambda doit être finale ou effectivement finale

134

La variable utilisée dans l'expression lambda doit être finale ou effectivement finale

Lorsque j'essaye de l'utiliser, calTzcette erreur s'affiche.

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;
}
user3610470
la source
5
Vous ne pouvez pas modifier à calTzpartir du lambda.
Elliott Frisch
2
J'ai supposé que c'était l'une de ces choses qui n'avaient tout simplement pas été faites à temps pour Java 8. Mais Java 8 était en 2014. Scala et Kotlin l'ont autorisé pendant des années, donc c'est évidemment possible. Java envisage-t-il d'éliminer cette restriction étrange?
GlenPeterson
5
Voici le lien mis à jour vers le commentaire de @MSDousti.
geisterfurz007
Je pense que vous pouvez utiliser Completable Futures comme solution de contournement.
Kraulain
Une chose importante que j'ai observée - vous pouvez utiliser des variables statiques au lieu de variables normales (cela le rend effectivement définitif, je suppose)
kaushalpranav

Réponses:

68

Une finalvariable signifie qu'elle ne peut être instanciée qu'une seule fois. en Java, vous ne pouvez pas utiliser de variables non finales dans lambda ainsi que dans les classes internes anonymes.

Vous pouvez refactoriser votre code avec l'ancienne boucle 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;
}

Même si je n'ai pas le sens de certains morceaux de ce code:

  • vous appelez a v.getTimeZoneId();sans utiliser sa valeur de retour
  • avec l'affectation, calTz = TimeZone.getTimeZone(v.getTimeZoneId().getValue());vous ne modifiez pas le passé initialement calTzet vous ne l'utilisez pas dans cette méthode
  • Vous revenez toujours null, pourquoi ne pas définir voidcomme type de retour?

J'espère aussi que ces conseils vous aideront à vous améliorer.

Francesco Pitzalis
la source
nous pouvons utiliser des variables statiques non finales
Narendra Jaggi
93

Bien que d'autres réponses prouvent l'exigence, elles n'expliquent pas pourquoi l'exigence existe.

Le JLS mentionne pourquoi au §15.27.2 :

La restriction aux variables effectivement finales interdit l'accès à des variables locales à changement dynamique, dont la capture introduirait probablement des problèmes de concurrence.

Pour réduire le risque de bogues, ils ont décidé de s'assurer que les variables capturées ne sont jamais mutées.

Dioxine
la source
11
Bonne réponse +1, et je suis surpris de voir à quel point la raison d' être effectivement final semble être limitée. À noter: une variable locale ne peut être capturée par un lambda que si elle est également définitivement affectée avant le corps du lambda. Les deux exigences semblent garantir que l'accès à la variable locale serait thread-safe.
Tim Biegeleisen le
2
une idée pourquoi cela est limité uniquement aux variables locales, et non aux membres de la classe? Je me retrouve souvent à contourner le problème en déclarant ma variable en tant que membre de la classe ...
David Refaeli
4
Les membres de la classe @DavidRefaeli sont couverts / affectés par le modèle de mémoire qui, s'il est suivi, produira des résultats prévisibles lorsqu'il est partagé. Les variables locales ne le sont pas, comme mentionné au §17.4.1
Dioxine
C'est un hack idiot, qui devrait être supprimé. Le compilateur doit avertir de l'accès potentiel aux variables cross-thread, mais doit l'autoriser. Ou, devrait être assez intelligent pour savoir si votre lambda fonctionne sur le même thread, ou fonctionne en parallèle, etc. C'est une limitation idiote, qui me rend triste. Et comme d'autres l'ont mentionné, les problèmes n'existent pas par exemple dans C #.
Josh M.
@JoshM. C # vous permet également de créer des types de valeurs modifiables , que les gens recommandent d'éviter pour éviter les problèmes. Au lieu d'avoir de tels principes, Java a décidé de l'empêcher complètement. Cela réduit les erreurs des utilisateurs, au prix de la flexibilité. Je ne suis pas d'accord avec cette restriction, mais c'est justifiable. La prise en compte du parallélisme nécessiterait un travail supplémentaire de la part du compilateur, ce qui explique probablement pourquoi la route " avertir l'accès cross-thread " n'a pas été prise. Un développeur travaillant sur la spécification serait probablement notre seule confirmation pour cela.
Dioxine le
58

À partir d'un lambda, vous ne pouvez pas obtenir de référence à tout ce qui n'est pas définitif. Vous devez déclarer un wrapper final de l'extérieur de la lamda pour contenir votre variable.

J'ai ajouté le dernier objet «référence» comme emballage.

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
la source
Je pensais à la même approche ou à une approche similaire - mais j'aimerais voir des conseils / commentaires d'experts sur cette réponse?
YoYo
4
Ce code manque une initiale reference.set(calTz);ou la référence doit être créée en utilisant new AtomicReference<>(calTz), sinon la TimeZone non nulle fournie en paramètre sera perdue.
Julien Kronegg
8
Cela devrait être la première réponse. Un AtomicReference (ou une classe Atomic___ similaire) contourne cette limitation en toute sécurité dans toutes les circonstances possibles.
GlenPeterson
1
D'accord, cela devrait être la réponse acceptée. Les autres réponses donnent des informations utiles sur la façon de revenir à un modèle de programmation non fonctionnel et sur les raisons pour lesquelles cela a été fait, mais ne vous disent pas vraiment comment contourner le problème!
Jonathan Benn
2
@GlenPeterson et est également une décision terrible, non seulement elle est beaucoup plus lente de cette façon, mais vous ignorez également la propriété d'effets secondaires que la documentation impose.
Eugene
41

Java 8 a un nouveau concept appelé variable «Effectivement final». Cela signifie qu'une variable locale non finale dont la valeur ne change jamais après l'initialisation est appelée «effectivement finale».

Ce concept a été introduit car avant Java 8 , nous ne pouvions pas utiliser de variable locale non finale dans une classe anonyme . Si vous voulez avoir accès à une variable locale dans une classe anonyme , vous devez la rendre définitive.

Lorsque lambda a été introduit, cette restriction a été assouplie. D'où la nécessité de rendre la variable locale finale si elle n'est pas modifiée une fois qu'elle est initialisée en tant que lambda en soi n'est rien d'autre qu'une classe anonyme.

Java 8 s'est rendu compte de la douleur de déclarer la variable locale final à chaque fois qu'un développeur utilisait lambda, a introduit ce concept et a rendu inutile la création de variables locales définitives. Donc, si vous voyez que la règle pour les classes anonymes n'a pas changé, c'est juste que vous n'avez pas à écrire le finalmot - clé à chaque fois lorsque vous utilisez lambdas.

J'ai trouvé une bonne explication ici

Dinesh Arora
la source
Le formatage du code ne doit être utilisé que pour le code , pas pour les termes techniques en général. effectively finaln'est pas du code, c'est de la terminologie. Voir Quand le formatage de code doit-il être utilisé pour le texte non codé? sur Meta Stack Overflow .
Charles Duffy
(Donc "le finalmot - clé" est un mot de code et il est correct de formater de cette façon, mais lorsque vous utilisez "final" de manière descriptive plutôt que comme code, c'est plutôt une terminologie).
Charles Duffy
9

Dans votre exemple, vous pouvez remplacer le forEachpar lamdba par une simple forboucle et modifier n'importe quelle variable librement. Ou, probablement, refactorisez votre code afin que vous n'ayez pas besoin de modifier les variables. Cependant, je vais vous expliquer par souci d'exhaustivité ce que signifie l'erreur et comment la contourner.

Spécification du langage Java 8, §15.27.2 :

Toute variable locale, paramètre formel ou paramètre d'exception utilisé mais non déclaré dans une expression lambda doit être soit déclaré final, soit être effectivement final ( §4.12.4 ), ou une erreur de compilation se produit lorsque l'utilisation est tentée.

Fondamentalement, vous ne pouvez pas modifier une variable locale ( calTzdans ce cas) à partir d'un lambda (ou d'une classe locale / anonyme). Pour y parvenir en Java, vous devez utiliser un objet mutable et le modifier (via une variable finale) à partir du lambda. Un exemple d'objet mutable ici serait un tableau d'un élément:

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];
}
Alexandre Udalov
la source
Une autre façon consiste à utiliser un champ d'un objet. Par exemple, MyObj result = new MyObj (); ...; result.timeZone = ...; ....; return result.timezone; Notez cependant que, comme expliqué ci-dessus, cela vous expose à des problèmes de sécurité des threads. Voir stackoverflow.com/a/50341404/7092558
Gibezynu Nu
0

s'il n'est pas nécessaire de modifier la variable, une solution de contournement générale pour ce genre de problème consisterait à extraire la partie de code qui utilise lambda et utilise le mot-clé final sur le paramètre de méthode.

robie2011
la source
0

Une variable utilisée dans l'expression lambda doit être un final ou effectivement final, mais vous pouvez attribuer une valeur à un dernier tableau d'élément.

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
la source
Ceci est très similaire à la suggestion d'Alexandre Udalov. En dehors de cela, je pense que cette approche repose sur les effets secondaires.
Scratte le