web-dev-qa-db-fra.com

Modification de la variable locale depuis lambda

La modification d'une variable locale dans forEach génère 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?

46
Patan

Utilisez un emballage

Avec Java 8+ :

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

Avec Java 10+ :

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

Si vous avez seulement besoin de transmettre la valeur de l'extérieur dans le lambda et de ne pas l'extraire, vous pouvez le faire avec une classe anonyme anonyme au lieu d'un lambda:

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

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

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

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

Comme les variables utilisées en dehors de la lamda doivent être (implicitement) définitives, vous devez utiliser quelque chose comme AtomicInteger ou écrire votre propre structure de données.

Voir https://docs.Oracle.com/javase/tutorial/Java/javaOO/lambdaexpressions.html#accessing-local-variables .

7
flo

Une alternative à AtomicInteger (ou à tout autre objet pouvant stocker une valeur) consiste à utiliser un tableau:

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

Mais voir la réponse de Stuart : il pourrait y avoir un meilleur moyen de traiter votre cas.

3
zakmck

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 faire ceci:

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 ira. 

2
Almir Campos

Si vous utilisez Java 10, vous pouvez utiliser var pour cela:

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

Vous pouvez le résumer pour éviter le compilateur, mais rappelez-vous que les effets secondaires chez les lambdas sont découragés. 

Pour citer le javadoc

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

0
user2153465

J'ai eu un problème légèrement différent. Au lieu d’incrémenter une variable locale dans le forEach, je devais affecter un objet à la variable locale.

J'ai résolu ce problème en définissant une classe de domaine interne privée qui englobe à la fois la liste que je veux parcourir (countryList) et le résultat que j'espère obtenir de cette liste (foundCountry). Puis, en utilisant Java 8 "forEach", je parcours le champ de liste et, lorsque l’objet que je souhaite est trouvé, j’assigne cet objet au champ de sortie. Donc, cela affecte une valeur à un champ de la variable locale, sans changer la variable locale elle-même. Je crois que puisque la variable locale elle-même n’a pas changé, 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;
    }
}

Itération opération:

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'erreur de compilation en laissant ces détails tels quels.

0
Steve T

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

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 spécifique, ce n'est pas une bonne solution, étant donné que Integer est pire que int pour vous, de toute façon, cette solution est plus générale, je pense.

0
luca.vercelli