web-dev-qa-db-fra.com

Lambdas: les variables locales ont besoin de final, les variables d'instance n'en ont pas

Dans un lambda, les variables locales doivent être finales, mais pas les variables d'instance. Pourquoi

64
Gerard

La différence fondamentale entre un champ et une variable locale est que la variable locale est copiée lorsque la machine virtuelle Java crée une instance lambda. D'autre part, les champs peuvent être modifiés librement, car leurs modifications sont également propagées à l'instance de classe externe (leur portée est la totalité de l'extérieur classe, comme Boris l'a souligné ci-dessous).

La manière la plus simple de penser aux classes anonymes, aux clôtures et aux labmdas est de la perspective variable ; Imaginez un constructeur de copie ajouté pour toutes les variables locales que vous transmettez à une clôture.

55
Adam Adamaszek

Dans le document du projet lambda: Etat du Lambda v4

Sous Section 7. Capture variable , il est mentionné que ....

Notre intention est d’interdire la capture de variables locales mutables. La raison en est que les idiomes aiment ceci:

int sum = 0;
list.forEach(e -> { sum += e.size(); });

sont fondamentalement en série; il est assez difficile d’écrire des corps lambda comme celui-ci qui n’ont pas de conditions de compétition. À moins que nous ne voulions imposer, de préférence au moment de la compilation, qu'une telle fonction ne puisse échapper à son thread de capture, cette fonctionnalité risque de causer plus de problèmes qu'elle n'en résout.

Modifier:

Une autre chose à noter ici est que les variables locales sont passées dans le constructeur de la classe interne lorsque vous y accédez au sein de votre classe interne, et cela ne fonctionnera pas avec la variable non finale car la valeur des variables non finales peut être modifiée après la construction.

Alors que dans le cas d'une variable d'instance, le compilateur passe à la référence de la classe et la référence de la classe sera utilisée pour accéder à la variable d'instance. Donc, ce n'est pas nécessaire dans le cas de variables d'instance.

PS: Il est à noter que les classes anonymes ne peuvent accéder qu'aux variables locales finales (dans Java SE 7), tandis que dans Java SE 8, vous pouvez y accéder efficacement). variables finales aussi à l'intérieur de lambda ainsi que des classes internes.

22
Not a bug

Dans Java 8 in Action book, cette situation s’explique par:

Vous vous demandez peut-être pourquoi les variables locales ont ces restrictions. Premièrement, il existe une différence essentielle dans la manière dont les variables d’instance et locales sont implémentées en arrière-plan. Les variables d'instance sont stockées sur le tas, alors que les variables locales résident sur la pile. Si un lambda pouvait accéder directement à la variable locale et que le lambda était utilisé dans un thread, le thread utilisant le lambda pouvait essayer d'accéder à la variable après que le thread qui avait alloué la variable l'avait désallouée. Ainsi, Java implémente l'accès à une variable locale libre en tant qu'accès à une copie de celle-ci plutôt qu'à la variable d'origine. Cela ne fait aucune différence si la variable locale n'est affectée qu'une seule fois - d'où Deuxièmement, cette restriction décourage également les modèles de programmation impératifs typiques (qui, comme nous l'expliquons dans les chapitres suivants, empêchent la parallélisation facile) qui mutent une variable externe.

14
sedooe

Les variables d’instance étant toujours accessibles via une opération d’accès aux champs sur une référence à un objet, c.-à-d. some_expression.instance_variable. Même lorsque vous n'y accédez pas explicitement via la notation par points, comme instance_variable, il est implicitement traité comme this.instance_variable (ou si vous êtes dans une classe interne et que vous accédez à la variable d'instance d'une classe externe, OuterClass.this.instance_variable, qui est sous le capot this.<hidden reference to outer this>.instance_variable).

Ainsi, une variable d'instance n'est jamais directement accessible, et la véritable "variable" à laquelle vous accédez directement est this (qui est "effectivement finale" car elle n'est pas assignable), ou une variable au début de certaines autres expression.

14
newacct

Mettre en place des concepts pour les futurs visiteurs:

En gros, tout se résume au point que le compilateur devrait être en mesure de dire de manière déterministe que le corps de l'expression lambda ne fonctionne pas sur une copie périmée des variables .

Dans le cas de variables locales, le compilateur n'a aucun moyen de s'assurer que le corps de l'expression lambda ne fonctionne pas sur une copie périmée de la variable, sauf si cette variable est finale ou effectivement finale. Les variables locales doivent donc être finales ou effectivement définitives.

Maintenant, dans le cas de champs d'instance, lorsque vous accédez à un champ d'instance dans l'expression lambda, le compilateur ajoutera alors un this à cet accès de variable (si vous ne l'avez pas fait explicitement) et depuis this est effectivement final, le compilateur est donc sûr que le corps de l'expression lambda aura toujours la dernière copie de la variable (veuillez noter que le multi-threading est hors de portée actuellement pour cette discussion). Ainsi, dans les cas où les champs d'instance, le compilateur peut dire que le corps lambda a la dernière copie de variable d'instance, les variables d'instance ne doivent pas nécessairement être finales ou réellement finales. Veuillez vous reporter à la capture d'écran ci-dessous d'une diapositive Oracle:

enter image description here

Veuillez également noter que si vous accédez à un champ d'instance dans une expression lambda et qu'il s'exécute dans un environnement multithread, vous pouvez potentiellement exécuter un problème.

11
hagrawal

Il semble que vous posiez des questions sur les variables que vous pouvez référencer à partir d'un corps lambda.

À partir de JLS §15.27.2

Toute variable locale, paramètre formel ou paramètre d'exception utilisé mais non déclaré dans une expression lambda doit soit être déclaré final, soit être réellement final (§4.12.4), sinon une erreur de compilation survient lors de la tentative d'utilisation.

Donc, vous n'avez pas besoin de déclarer les variables en tant que final il vous suffit de vous assurer qu'elles sont "effectivement finales". C'est la même règle que pour les classes anonymes.

9
Boris the Spider

Dans les expressions Lambda, vous pouvez utiliser efficacement les variables finales de la portée environnante. Cela signifie effectivement qu'il n'est pas obligatoire de déclarer la variable finale, mais de ne pas modifier son état dans l'expression lambda.

Vous pouvez également utiliser ceci dans les fermetures et utiliser "ceci" signifie l'objet englobant mais pas le lambda lui-même car les fermetures sont des fonctions anonymes et aucune classe ne leur est associée.

Ainsi, lorsque vous utilisez n’importe quel champ (disons Private Integer i;) de la classe englobante qui n’est pas déclarée finale ni réellement finale, il fonctionnera tout de même car le compilateur fait le tour de votre choix et insère "this" (this.i) .

private Integer i = 0;
public  void process(){
    Consumer<Integer> c = (i)-> System.out.println(++this.i);
    c.accept(i);
}
6
nasioman

Voici un exemple de code, comme je ne m'y attendais pas non plus, je m'attendais à ne pouvoir rien modifier en dehors de mon lambda

 public class LambdaNonFinalExample {
    static boolean odd = false;

    public static void main(String[] args) throws Exception {
       //boolean odd = false; - If declared inside the method then I get the expected "Effectively Final" compile error
       runLambda(() -> odd = true);
       System.out.println("Odd=" + odd);
    }

    public static void runLambda(Callable c) throws Exception {
       c.call();
    }

 }

Sortie: Odd = true

5
losty

OUI, vous pouvez modifier les variables de membre de l'instance, mais vous [~ # ~] ne pouvez pas [~ # ~ ] change l'instance elle-même comme lorsque vous manipulez des variables .

Quelque chose comme ça comme mentionné:

    class Car {
        public String name;
    }

    public void testLocal() {
        int theLocal = 6;
        Car bmw = new Car();
        bmw.name = "BMW";
        Stream.iterate(0, i -> i + 2).limit(2)
        .forEach(i -> {
//            bmw = new Car(); // LINE - 1;
            bmw.name = "BMW NEW"; // LINE - 2;
            System.out.println("Testing local variables: " + (theLocal + i));

        });
        // have to comment this to ensure it's `effectively final`;
//        theLocal = 2; 
    }

Le principe de base pour restreindre les variables locales est d'environ validité des données et des calculs

Si le lambda, évalué par le second thread, avait la possibilité de muter des variables locales. Même la capacité de lire la valeur de variables locales mutables d’un autre thread introduirait la nécessité d’une synchronisation ou de l’utilisation de volatile afin d'éviter de lire des données obsolètes.

Mais comme nous savons le but principal des lambdas

Parmi les différentes raisons à cela, la plus pressante pour la plate-forme Java) est qu’elles facilitent la distribution du traitement des collections sur plusieurs threads .

Contrairement aux variables locales, l'instance locale peut être mutée, car elle est partagée . globalement. Nous pouvons mieux comprendre cela via le différence entre tas et pile :

Chaque fois qu'un objet est créé, il est toujours stocké dans l'espace de tas et la mémoire de pile contient la référence. La mémoire de pile ne contient que des variables primitives locales et des variables de référence aux objets dans l'espace de tas.

Donc pour résumer, il y a deux points qui, je pense, comptent vraiment:

  1. Il est très difficile de créer l’instance effectivement définitive, ce qui pourrait causer beaucoup de fardeau insensé (imaginez simplement le classe imbriquée);

  2. l'instance elle-même est déjà partagée globalement et lambda est également partageable entre les threads, afin qu'ils puissent fonctionner ensemble correctement, car nous savons que nous gérons la mutation et que vous souhaitez passer cette mutation autour;

Le point d'équilibre est clair: si vous savez ce que vous faites, vous pouvez le faire facilement mais sinon, le ) la restriction par défaut aidera à éviter les bugs insidieux.

P.S. Si la synchronisation est requise dans la mutation d'instance , vous pouvez utiliser directement le méthodes de réduction de flux ou s'il y a un problème de dépendance dans mutation d'instance , vous pouvez toujours utiliser thenApply ou thenCompose dans Fonction tandis que mapping ou des méthodes similaires.

4
Hearen