Modification de la variable locale depuis l'intérieur de lambda


115

La modification d'une variable locale dans forEachdonne une erreur de compilation:

Ordinaire

    int ordinal = 0;
    for (Example s : list) {
        s.setOrdinal(ordinal);
        ordinal++;
    }

Avec Lambda

    int ordinal = 0;
    list.forEach(s -> {
        s.setOrdinal(ordinal);
        ordinal++;
    });

aucune idée pour résoudre ça?


8
Considérant que les lambdas sont essentiellement du sucre syntaxique pour une classe interne anonyme, mon intuition est qu'il est impossible de capturer une variable locale non finale. J'adorerais avoir tort cependant.
Sinkingpoint

2
Une variable utilisée dans une expression lambda doit être effectivement finale. Vous pouvez utiliser un entier atomique bien qu'il soit excessif, donc une expression lambda n'est pas vraiment nécessaire ici. Tenez-vous-en à la boucle for.
Alexis C.

3
La variable doit être effectivement définitive . Voir ceci: Pourquoi la restriction sur la capture de variables locales?
Jesper


2
@Quirliom Ils ne sont pas du sucre syntaxique pour les classes anonymes. Les lambdas utilisent des poignées de méthode sous le capot
Dioxine

Réponses:


177

Utilisez un emballage

Tout type d'emballage est bon.

Avec Java 8+ , utilisez un AtomicInteger:

AtomicInteger ordinal = new AtomicInteger(0);
list.forEach(s -> {
  s.setOrdinal(ordinal.getAndIncrement());
});

... ou un tableau:

int[] ordinal = { 0 };
list.forEach(s -> {
  s.setOrdinal(ordinal[0]++);
});

Avec Java 10+ :

var wrapper = new Object(){ int ordinal = 0; };
list.forEach(s -> {
  s.setOrdinal(wrapper.ordinal++);
});

Remarque: soyez très prudent si vous utilisez un flux parallèle. Vous pourriez ne pas vous retrouver avec le résultat attendu. D'autres solutions comme celle de Stuart pourraient être plus adaptées à ces cas.

Pour les types autres que int

Bien sûr, cela est toujours valable pour les types autres que int. Il vous suffit de changer le type d'habillage en un AtomicReferenceou un tableau de ce type. Par exemple, si vous utilisez a String, procédez comme suit:

AtomicReference<String> value = new AtomicReference<>();
list.forEach(s -> {
  value.set("blah");
});

Utilisez un tableau:

String[] value = { null };
list.forEach(s-> {
  value[0] = "blah";
});

Ou avec Java 10+:

var wrapper = new Object(){ String value; }
list.forEach(s->{
  wrapper.value = "blah";
});

Pourquoi êtes-vous autorisé à utiliser un tableau comme int[] ordinal = { 0 };? Pouvez-vous l'expliquer. Merci
mrbela

@mrbela Qu'est-ce que tu ne comprends pas exactement? Peut-être puis-je clarifier ce point précis?
Olivier Grégoire

1
Les tableaux @mrbela sont passés par référence. Lorsque vous passez dans un tableau, vous transmettez en fait une adresse mémoire pour ce tableau. Les types primitifs comme les entiers sont envoyés par valeur, ce qui signifie qu'une copie de la valeur est transmise. Pass-par-valeur il n'y a pas de relation entre la valeur d'origine et la copie envoyée dans vos méthodes - la manipulation de la copie ne fait rien à l'original. Passer par référence signifie que la valeur d'origine et celle envoyée à la méthode sont identiques - la manipuler dans votre méthode changera la valeur en dehors de la méthode.
DetectivePikachu

@Olivier Grégoire cela m'a vraiment sauvé la peau. J'ai utilisé la solution Java 10 avec "var". Pouvez-vous expliquer comment cela fonctionne? J'ai googlé partout et c'est le seul endroit que j'ai trouvé avec cette utilisation particulière. Je suppose que comme un lambda n'autorise (effectivement) que les objets finaux hors de sa portée, l'objet "var" trompe fondamentalement le compilateur en lui faisant déduire qu'il est définitif, puisqu'il suppose que seuls les objets finaux seront référencés en dehors de la portée. Je ne sais pas, c'est ma meilleure estimation. Si vous souhaitez fournir une explication, je l'apprécierais, car j'aime savoir pourquoi mon code fonctionne :)
oaker

1
@oaker Cela fonctionne car Java créera la classe MyClass $ 1 comme suit: class MyClass$1 { int value; }Dans une classe habituelle, cela signifie que vous créez une classe avec une variable privée de package nommée value. C'est la même chose, mais la classe est en fait anonyme pour nous, pas pour le compilateur ou la JVM. Java compilera le code comme s'il l'était MyClass$1 wrapper = new MyClass$1();. Et la classe anonyme devient juste une autre classe. En fin de compte, nous venons d'ajouter du sucre syntaxique par-dessus pour le rendre lisible. En outre, la classe est une classe interne avec un champ package-private. Ce champ est utilisable.
Olivier Grégoire

14

C'est assez proche d'un problème XY . Autrement dit, la question posée est essentiellement de savoir comment muter une variable locale capturée à partir d'un lambda. Mais la tâche actuelle consiste à numéroter les éléments d'une liste.

D'après mon expérience, plus de 80% du temps, il y a une question de savoir comment muter un local capturé à partir d'un lambda, il y a une meilleure façon de procéder. Cela implique généralement une réduction, mais dans ce cas, la technique d'exécution d'un flux sur les index de liste s'applique bien:

IntStream.range(0, list.size())
         .forEach(i -> list.get(i).setOrdinal(i));

3
Bonne solution mais seulement si listest une RandomAccessliste
ZhekaKozlov

Je serais curieux de savoir si le problème du message de progression toutes les k itérations peut être pratiquement résolu sans compteur dédié, c'est-à-dire dans ce cas: stream.forEach (e -> {doSomething (e); if (++ ctr% 1000 = = 0) log.info ("J'ai traité {} éléments", ctr);} Je ne vois pas de moyen plus pratique (la réduction pourrait le faire, mais serait plus verbeuse, surtout avec les flux parallèles).
zakmck

14

Si vous avez seulement besoin de passer la valeur de l'extérieur dans le lambda, et de ne pas la sortir, vous pouvez le faire avec une classe anonyme régulière au lieu d'un lambda:

list.forEach(new Consumer<Example>() {
    int ordinal = 0;
    public void accept(Example s) {
        s.setOrdinal(ordinal);
        ordinal++;
    }
});

1
... et si vous avez besoin de lire le résultat? Le résultat n'est pas visible pour le code au niveau supérieur.
Luke Usherwood

@LukeUsherwood: Vous avez raison. Ce n'est que si vous avez seulement besoin de transmettre des données de l'extérieur vers le lambda, pas de les sortir. Si vous avez besoin de le sortir, vous devez avoir le lambda capturer une référence à un objet mutable, par exemple un tableau, ou un objet avec des champs publics non finaux, et passer des données en le définissant dans l'objet.
newacct


3

Si vous êtes sur Java 10, vous pouvez utiliser varpour cela:

var ordinal = new Object() { int value; };
list.forEach(s -> {
    s.setOrdinal(ordinal.value);
    ordinal.value++;
});

3

Une alternative à AtomicInteger(ou à tout autre objet capable de stocker une valeur) est d'utiliser un tableau:

final int ordinal[] = new int[] { 0 };
list.forEach ( s -> s.setOrdinal ( ordinal[ 0 ]++ ) );

Mais voyez la réponse de Stuart : il y a peut-être une meilleure façon de traiter votre cas.


2

Je sais que c'est une vieille question, mais si vous êtes à l'aise avec une solution de contournement, compte tenu du fait que la variable externe doit être finale, vous pouvez simplement le faire:

final int[] ordinal = new int[1];
list.forEach(s -> {
    s.setOrdinal(ordinal[0]);
    ordinal[0]++;
});

Peut-être pas le plus élégant, ni même le plus correct, mais ça fera l'affaire.


1

Vous pouvez le résumer pour contourner le compilateur, mais n'oubliez pas que les effets secondaires dans les lambdas sont déconseillés.

Pour citer le javadoc

Les effets secondaires des paramètres comportementaux pour les opérations de flux sont, en général, découragés, car ils peuvent souvent conduire à des violations involontaires de l'exigence d'apatridie Un petit nombre d'opérations de flux, telles que forEach () et peek (), ne peuvent fonctionner que via side -effets; ceux-ci doivent être utilisés avec précaution


0

J'ai eu un problème légèrement différent. Au lieu d'incrémenter une variable locale dans forEach, j'avais besoin d'assigner un objet à la variable locale.

J'ai résolu ce problème en définissant une classe de domaine interne privé qui englobe à la fois la liste sur laquelle je veux parcourir (countryList) et la sortie que j'espère obtenir de cette liste (foundCountry). Ensuite, en utilisant Java 8 "forEach", j'itère sur le champ de liste, et lorsque l'objet que je veux est trouvé, j'attribue cet objet au champ de sortie. Ainsi, cela affecte une valeur à un champ de la variable locale, sans modifier la variable locale elle-même. Je crois que puisque la variable locale elle-même n'est pas modifiée, le compilateur ne se plaint pas. Je peux ensuite utiliser la valeur que j'ai capturée dans le champ de sortie, en dehors de la liste.

Objet de domaine:

public class Country {

    private int id;
    private String countryName;

    public Country(int id, String countryName){
        this.id = id;
        this.countryName = countryName;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getCountryName() {
        return countryName;
    }

    public void setCountryName(String countryName) {
        this.countryName = countryName;
    }
}

Objet wrapper:

private class CountryFound{
    private final List<Country> countryList;
    private Country foundCountry;
    public CountryFound(List<Country> countryList, Country foundCountry){
        this.countryList = countryList;
        this.foundCountry = foundCountry;
    }
    public List<Country> getCountryList() {
        return countryList;
    }
    public void setCountryList(List<Country> countryList) {
        this.countryList = countryList;
    }
    public Country getFoundCountry() {
        return foundCountry;
    }
    public void setFoundCountry(Country foundCountry) {
        this.foundCountry = foundCountry;
    }
}

Opération itérative:

int id = 5;
CountryFound countryFound = new CountryFound(countryList, null);
countryFound.getCountryList().forEach(c -> {
    if(c.getId() == id){
        countryFound.setFoundCountry(c);
    }
});
System.out.println("Country found: " + countryFound.getFoundCountry().getCountryName());

Vous pouvez supprimer la méthode de classe wrapper "setCountryList ()" et rendre le champ "countryList" final, mais je n'ai pas eu d'erreurs de compilation en laissant ces détails tels quels.


0

Pour avoir une solution plus générale, vous pouvez écrire une classe Wrapper générique:

public static class Wrapper<T> {
    public T obj;
    public Wrapper(T obj) { this.obj = obj; }
}
...
Wrapper<Integer> w = new Wrapper<>(0);
this.forEach(s -> {
    s.setOrdinal(w.obj);
    w.obj++;
});

(ceci est une variante de la solution donnée par Almir Campos).

Dans le cas précis ce n'est pas une bonne solution, comme Integerc'est pire que intpour votre propos, de toute façon cette solution est plus générale je pense.


0

Oui, vous pouvez modifier les variables locales à partir de lambdas (de la manière indiquée par les autres réponses), mais vous ne devriez pas le faire. Les lambdas ont été conçus pour un style de programmation fonctionnel et cela signifie: Aucun effet secondaire. Ce que vous voulez faire est considéré comme un mauvais style. C'est également dangereux en cas de flux parallèles.

Vous devez soit trouver une solution sans effets secondaires, soit utiliser une boucle for traditionnelle.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.