Modificando la variable local desde dentro de lambda

115

Modificar una variable local en forEachda un error de compilación:

Normal

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

Con Lambda

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

¿Alguna idea de cómo resolver esto?

Patán
fuente
8
Considerando que las lambdas son esencialmente azúcar sintáctica para una clase interna anónima, mi intuición es que es imposible capturar una variable local no final. Sin embargo, me encantaría que me demuestren que estoy equivocado.
Sinkingpoint
2
Una variable utilizada en una expresión lambda debe ser efectivamente final. Puede usar un entero atómico aunque es excesivo, por lo que aquí no se necesita una expresión lambda. Solo quédate con el bucle for.
Alexis C.
3
La variable debe ser efectivamente final . Vea esto: ¿Por qué la restricción en la captura de variables locales?
Jesper
2
@Quirliom No son azúcar sintáctico para clases anónimas. Lambdas usa manijas de método debajo del capó
Dioxina

Respuestas:

176

Usa una envoltura

Cualquier tipo de envoltorio es bueno.

Con Java 8+ , use un AtomicInteger:

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

... o una matriz:

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

Con Java 10+ :

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

Nota: tenga mucho cuidado si utiliza una transmisión en paralelo. Es posible que no obtenga el resultado esperado. Otras soluciones como la de Stuart podrían adaptarse mejor a esos casos.

Para tipos distintos a int

Por supuesto, esto sigue siendo válido para tipos distintos de int. Solo necesita cambiar el tipo de envoltura a AtomicReferenceuna matriz de ese tipo. Por ejemplo, si usa a String, simplemente haga lo siguiente:

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

Usa una matriz:

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

O con Java 10+:

var wrapper = new Object(){ String value; }
list.forEach(s->{
  wrapper.value = "blah";
});
Olivier Grégoire
fuente
¿Por qué se le permite usar una matriz como int[] ordinal = { 0 };? Puedes explicarlo. Gracias
mrbela
@mrbela ¿Qué es exactamente lo que no entiendes? ¿Quizás pueda aclarar ese bit específico?
Olivier Grégoire
1
Las matrices @mrbela se pasan por referencia. Cuando pasa una matriz, en realidad está pasando una dirección de memoria para esa matriz. Los tipos primitivos como el entero se envían por valor, lo que significa que se pasa una copia del valor. Pasar por valor no hay relación entre el valor original y la copia enviada a sus métodos; manipular la copia no afecta al original. Pasar por referencia significa que el valor original y el enviado al método son uno en el mismo; manipularlo en su método cambiará el valor fuera del método.
DetectivePikachu
@Olivier Grégoire esto realmente me salvó el pellejo. Usé la solución Java 10 con "var". ¿Puedes explicar cómo funciona esto? He buscado en Google por todas partes y este es el único lugar que he encontrado con este uso en particular. Mi suposición es que dado que una lambda solo permite (efectivamente) objetos finales fuera de su alcance, el objeto "var" básicamente engaña al compilador para que infiera que es final, ya que asume que solo los objetos finales serán referenciados fuera del alcance. No lo sé, esa es mi mejor suposición. Si quisiera proporcionar una explicación, lo agradecería, ya que me gustaría saber por qué funciona mi código :)
Oaker
1
@oaker Esto funciona porque Java creará la clase MyClass $ 1 de la siguiente manera: class MyClass$1 { int value; }En una clase habitual, esto significa que crea una clase con una variable privada de paquete denominada value. Esto es lo mismo, pero la clase es anónima para nosotros, no para el compilador o la JVM. Java compilará el código como si lo fuera MyClass$1 wrapper = new MyClass$1();. Y la clase anónima se convierte en una clase más. Al final, solo agregamos azúcar sintáctico encima para que sea legible. Además, la clase es una clase interna con un campo privado de paquete. Ese campo es utilizable.
Olivier Grégoire
14

Esto está bastante cerca de un problema XY . Es decir, la pregunta que se hace es esencialmente cómo mutar una variable local capturada de una lambda. Pero la tarea real que tenemos entre manos es cómo numerar los elementos de una lista.

En mi experiencia, más del 80% de las veces hay una cuestión de cómo mutar un local capturado desde dentro de una lambda, hay una mejor manera de proceder. Por lo general, esto implica una reducción, pero en este caso, la técnica de ejecutar una secuencia sobre los índices de la lista se aplica bien:

IntStream.range(0, list.size())
         .forEach(i -> list.get(i).setOrdinal(i));
Stuart Marks
fuente
3
Buena solución, pero solo si listes una RandomAccesslista
ZhekaKozlov
Tendría curiosidad por saber si el problema del mensaje de progresión cada k iteraciones se puede resolver prácticamente sin un contador dedicado, es decir, este caso: stream.forEach (e -> {doSomething (e); if (++ ctr% 1000 = = 0) log.info ("He procesado {} elementos", ctr);} No veo una forma más práctica (la reducción podría hacerlo, pero sería más detallado, especialmente con flujos paralelos).
zakmck
14

Si solo necesita pasar el valor desde el exterior a la lambda y no sacarlo, puede hacerlo con una clase anónima regular en lugar de una lambda:

list.forEach(new Consumer<Example>() {
    int ordinal = 0;
    public void accept(Example s) {
        s.setOrdinal(ordinal);
        ordinal++;
    }
});
newacct
fuente
1
... y ¿qué pasa si realmente necesitas leer el resultado? El resultado no es visible para el código en el nivel superior.
Luke Usherwood
@LukeUsherwood: Tienes razón. Esto es solo para si solo necesita pasar datos desde el exterior a la lambda, no sacarlos. Si necesita sacarlo, necesitará que la lambda capture una referencia a un objeto mutable, por ejemplo, una matriz, o un objeto con campos públicos no finales, y pase datos configurándolos en el objeto.
newacct
3

Si está en Java 10, puede usar varpara eso:

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

Una alternativa a AtomicInteger(o cualquier otro objeto capaz de almacenar un valor) es usar una matriz:

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

Pero vea la respuesta de Stuart : podría haber una mejor manera de lidiar con su caso.

zakmck
fuente
2

Sé que es una vieja pregunta, pero si se siente cómodo con una solución alternativa, considerando el hecho de que la variable externa debe ser definitiva, simplemente puede hacer esto:

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

Quizás no sea la más elegante, ni siquiera la más correcta, pero servirá.

Almir Campos
fuente
1

Puede resumirlo para solucionar el problema del compilador, pero recuerde que no se recomiendan los efectos secundarios en lambdas.

Para citar el javadoc

En general, se desaconsejan los efectos secundarios en los parámetros de comportamiento de las operaciones de transmisión, ya que a menudo pueden conducir a violaciones involuntarias del requisito de apatridia. -efectos; estos deben usarse con cuidado

codemonkey
fuente
0

Tuve un problema ligeramente diferente. En lugar de incrementar una variable local en forEach, necesitaba asignar un objeto a la variable local.

Resolví esto definiendo una clase de dominio interno privado que envuelve tanto la lista sobre la que quiero iterar (countryList) como la salida que espero obtener de esa lista (foundCountry). Luego, usando Java 8 "forEach", itero sobre el campo de lista, y cuando se encuentra el objeto que quiero, asigno ese objeto al campo de salida. Entonces esto asigna un valor a un campo de la variable local, sin cambiar la variable local en sí. Creo que dado que la variable local en sí no se cambia, el compilador no se queja. Luego puedo usar el valor que capturé en el campo de salida, fuera de la lista.

Objeto de dominio:

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;
    }
}

Objeto envoltorio:

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;
    }
}

Operación iterativa:

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());

Puede eliminar el método de la clase contenedora "setCountryList ()" y hacer que el campo "countryList" sea final, pero no obtuve errores de compilación dejando estos detalles como están.

Steve T
fuente
0

Para tener una solución más general, puede escribir una clase Wrapper genérica:

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++;
});

(esta es una variante de la solución dada por Almir Campos).

En el caso específico, esta no es una buena solución, ya que Integeres peor que intpara su propósito, de todos modos esta solución es más general, creo.

luca.vercelli
fuente
0

Sí, puede modificar las variables locales desde dentro de las lambdas (de la manera que se muestra en las otras respuestas), pero no debe hacerlo. Las lambdas han sido para un estilo funcional de programación y esto significa: Sin efectos secundarios. Lo que quieres hacer se considera de mal estilo. También es peligroso en caso de corrientes paralelas.

Debería encontrar una solución sin efectos secundarios o utilizar un bucle for tradicional.

Donat
fuente