Modificando a variável local de dentro do lambda

115

Modificar uma variável local em forEachdá um erro de compilação:

Normal

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

Com lambda

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

Alguma ideia de como resolver isso?

Patan
fonte
8
Considerando que lambdas são essencialmente açúcares sintáticos para uma classe interna anônima, minha intuição é que é impossível capturar uma variável local não final. Eu adoraria provar que estou errado.
Sinkingpoint
2
Uma variável usada em uma expressão lambda deve ser efetivamente final. Você poderia usar um número inteiro atômico, embora seja um exagero, portanto, uma expressão lambda não é realmente necessária aqui. Apenas fique com o loop for.
Alexis C.
3
A variável deve ser efetivamente final . Veja isto: Por que a restrição na captura de variáveis ​​locais?
Jesper
2
@Quirliom Eles não são açúcar sintático para classes anônimas. Lambdas usam alças de método sob o capô
Dioxina

Respostas:

177

Use um invólucro

Qualquer tipo de embalagem é bom.

Com o Java 8+ , use AtomicInteger:

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

... ou uma matriz:

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

Com Java 10+ :

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

Nota: tenha muito cuidado se você usar um fluxo paralelo. Você pode não acabar com o resultado esperado. Outras soluções como a de Stuart podem ser mais adaptadas para esses casos.

Para tipos diferentes de int

Claro, isso ainda é válido para tipos diferentes de int. Você só precisa alterar o tipo de embalagem para um AtomicReferenceou uma matriz desse tipo. Por exemplo, se você usar um String, faça o seguinte:

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

Use uma matriz:

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

Ou com Java 10+:

var wrapper = new Object(){ String value; }
list.forEach(s->{
  wrapper.value = "blah";
});
Olivier Grégoire
fonte
Por que você tem permissão para usar um array como int[] ordinal = { 0 };? Você pode explicar isso. Obrigado
mrbela
@mrbela O que exatamente você não entende? Talvez eu possa esclarecer essa parte específica?
Olivier Grégoire
1
Matrizes @mrbela são passadas por referência. Quando você passa um array, está na verdade passando um endereço de memória para aquele array. Tipos primitivos como inteiros são enviados por valor, o que significa que uma cópia do valor é passada. Passado por valor, não há relação entre o valor original e a cópia enviada em seus métodos - manipular a cópia não faz nada ao original. Passar por referência significa que o valor original e aquele enviado para o método são um no mesmo - manipulá-lo em seu método mudará o valor fora do método.
Detetive
@Olivier Grégoire isso realmente salvou minha pele. Usei a solução Java 10 com "var". Você pode explicar como isso funciona? Pesquisei em todos os lugares e este é o único lugar que encontrei com esse uso específico. Meu palpite é que, uma vez que um lambda só permite (efetivamente) objetos finais de fora de seu escopo, o objeto "var" basicamente engana o compilador, fazendo-o inferir que é final, uma vez que assume que apenas objetos finais serão referenciados fora do escopo. Não sei, é meu melhor palpite. Se você se importar em fornecer uma explicação, eu agradeceria, pois gostaria de saber por que meu código funciona :)
oaker
1
@oaker Isso funciona porque Java criará a classe MyClass $ 1 da seguinte maneira: class MyClass$1 { int value; }Em uma classe normal, isso significa que você cria uma classe com uma variável privada de pacote chamada value. É o mesmo, mas a classe é realmente anônima para nós, não para o compilador ou a JVM. Java irá compilar o código como se fosse MyClass$1 wrapper = new MyClass$1();. E a classe anônima se torna apenas outra classe. No final, apenas adicionamos açúcar sintático em cima para torná-lo legível. Além disso, a classe é uma classe interna com campo de pacote privado. Esse campo é utilizável.
Olivier Grégoire
14

Isso é quase um problema XY . Ou seja, a questão que está sendo feita é essencialmente como transformar uma variável local capturada de um lambda. Mas a tarefa real em questão é como numerar os elementos de uma lista.

Na minha experiência, em mais de 80% das vezes, há uma questão de como transformar um local capturado de dentro de um lambda, há uma maneira melhor de proceder. Normalmente, isso envolve redução, mas, neste caso, a técnica de executar um fluxo sobre os índices da lista se aplica bem:

IntStream.range(0, list.size())
         .forEach(i -> list.get(i).setOrdinal(i));
Stuart Marks
fonte
3
Boa solução, mas somente se listfor uma RandomAccesslista
ZhekaKozlov
Gostaria de saber se o problema da mensagem de progressão a cada k iterações pode ser praticamente resolvido sem um contador dedicado, ou seja, neste caso: stream.forEach (e -> {doSomething (e); if (++ ctr% 1000 = = 0) log.info ("Eu processei {} elementos", ctr);} Não vejo uma maneira mais prática (a redução poderia fazer isso, mas seria mais prolixo, especialmente com fluxos paralelos).
zakmck
14

Se você só precisa passar o valor de fora para o lambda, e não retirá-lo, pode fazer isso com uma classe anônima regular em vez de um lambda:

list.forEach(new Consumer<Example>() {
    int ordinal = 0;
    public void accept(Example s) {
        s.setOrdinal(ordinal);
        ordinal++;
    }
});
newacct
fonte
1
... e se você realmente precisar ler o resultado? O resultado não é visível para o código de nível superior.
Luke Usherwood
@LukeUsherwood: Você está certo. Isso é válido apenas se você só precisar passar dados de fora para o lambda, não retirá-los. Se precisar retirá-lo, você precisará fazer com que o lambda capture uma referência a um objeto mutável, por exemplo, uma matriz ou um objeto com campos públicos não finais, e passe os dados configurando-os no objeto.
newacct
3

Se você estiver no Java 10, pode usar varpara isso:

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

Uma alternativa para AtomicInteger(ou qualquer outro objeto capaz de armazenar um valor) é usar uma matriz:

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

Mas veja a resposta do Stuart : pode haver uma maneira melhor de lidar com o seu caso.

zakmck
fonte
2

Eu sei que é uma pergunta antiga, mas se você se sentir confortável com uma solução alternativa, considerando o fato de que a variável externa deve ser final, você pode simplesmente fazer isto:

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

Talvez não seja o mais elegante, ou mesmo o mais correto, mas servirá.

Almir Campos
fonte
1

Você pode embrulhar para contornar o compilador, mas lembre-se de que os efeitos colaterais em lambdas são desencorajados.

Para citar o javadoc

Os efeitos colaterais em parâmetros comportamentais para operações de fluxo são, em geral, desencorajados, pois podem frequentemente levar a violações involuntárias do requisito de apatridia. Um pequeno número de operações de fluxo, como forEach () e peek (), podem operar apenas via lado -efeitos; estes devem ser usados ​​com cuidado

codemonkey
fonte
0

Eu tive um problema um pouco diferente. Em vez de incrementar uma variável local em forEach, precisei atribuir um objeto à variável local.

Resolvi isso definindo uma classe de domínio interno privado que envolve a lista que desejo iterar (countryList) e a saída que espero obter dessa lista (foundCountry). Em seguida, usando Java 8 "forEach", itero sobre o campo de lista e, quando o objeto que desejo é encontrado, atribuo esse objeto ao campo de saída. Portanto, isso atribui um valor a um campo da variável local, não alterando a própria variável local. Acredito que, como a variável local em si não é alterada, o compilador não reclama. Posso então usar o valor que capturei no campo de saída, fora da lista.

Objeto de domínio:

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

Operação de iteração:

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

Você poderia remover o método da classe wrapper "setCountryList ()" e tornar o campo "countryList" final, mas não recebi erros de compilação deixando esses detalhes como estão.

Steve T
fonte
0

Para ter uma solução mais geral, você pode escrever uma classe 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 é uma variante da solução dada por Almir Campos).

No caso específico esta não é uma boa solução, pois Integeré pior do que intpara o seu propósito, de qualquer forma esta solução é mais geral eu acho.

luca.vercelli
fonte
0

Sim, você pode modificar variáveis ​​locais de dentro dos lambdas (da maneira mostrada pelas outras respostas), mas você não deve fazer isso. Lambdas são para um estilo funcional de programação e isso significa: Sem efeitos colaterais. O que você quer fazer é considerado um estilo ruim. Também é perigoso no caso de fluxos paralelos.

Você deve encontrar uma solução sem efeitos colaterais ou usar um loop for tradicional.

Donat
fonte