Подтвердить что ты не робот

Изменение локальной переменной изнутри лямбда

Изменение локальной переменной в forEach дает ошибку компиляции:

Normal

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

С Lambda

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

Любая идея, как решить эту проблему?

4b9b3361

Ответ 1

Используйте обертку

В Java 8+ используйте либо AtomicInteger:

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

... или массив:

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

С Java 10+:

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

Примечание: будьте очень осторожны, если вы используете параллельный поток. Вы можете не получить ожидаемый результат. Другие решения, такие как Stuart, могут быть более адаптированы для этих случаев.

Для типов, отличных от int

Конечно, это все еще действительно для типов кроме int. Вам нужно только изменить тип упаковки на AtomicReference или массив этого типа. Например, если вы используете String, просто сделайте следующее:

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

Используйте массив:

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

Или с Java 10+:

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

Ответ 2

Это довольно близко к проблеме XY. То есть, задаваемый вопрос заключается в том, как мутировать захваченную локальную переменную из лямбда. Но фактическая задача заключается в том, как подсчитать элементы списка.

По моему опыту, в 80% случаев возникает вопрос о том, как мутировать захваченный локальный изнутри лямбда, есть лучший способ продолжения. Обычно это связано с сокращением, но в этом случае техника управления потоком по индексам списка применяется хорошо:

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

Ответ 3

Если вам нужно только передать значение извне в лямбду, а не вывести его, вы можете сделать это с помощью обычного анонимного класса вместо лямбды:

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

Ответ 5

Альтернативой AtomicInteger (или любому другому объекту, способному хранить значение) является использование массива:

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

Но посмотрите ответ Стюарта: возможно, есть лучший способ разобраться с вашим делом.

Ответ 6

Если вы используете Java 10, вы можете использовать var для этого:

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

Ответ 7

Я знаю, что это старый вопрос, но если вас устраивает обходной путь, учитывая тот факт, что внешняя переменная должна быть конечной, вы можете просто сделать это:

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

Может быть, не самый элегантный или даже самый правильный, но это подойдет.

Ответ 8

Вы можете обернуть его, чтобы обойти компилятор, но, пожалуйста, помните, что побочные эффекты в лямбдах не поощряются.

Чтобы процитировать javadoc

Побочные эффекты в поведенческих параметрах для операций с потоком, как правило, обескуражены, поскольку они часто могут приводить к невольным нарушениям требования о безгражданстве Небольшое количество операций потока, таких как forEach() и peek(), может работать только с помощью побочных эффектов; их следует использовать с осторожностью

Ответ 9

У меня была немного другая проблема. Вместо увеличения локальной переменной в forEach мне нужно было назначить объект локальной переменной.

Я решил это, определив частный внутренний класс домена, который обертывает как список, который я хочу перебирать (countryList), так и вывод, который я надеюсь получить из этого списка (foundCountry). Затем, используя Java 8 "forEach", я перебираю по полю списка, и когда найден объект, который я хочу, я назначаю этот объект в поле вывода. Таким образом, это присваивает значение полю локальной переменной, не меняя локальную переменную. Я считаю, что, поскольку локальная переменная сама по себе не изменяется, компилятор не жалуется. Затем я могу использовать значение, которое я захватил в поле вывода, вне списка.

Объект домена:

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

Объект обертки:

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

Итерация:

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

Вы можете удалить метод класса-оболочки "setCountryList()" и сделать поле "countryList" окончательным, но я не получил ошибок компиляции, оставляя эти данные как есть.

Ответ 10

Чтобы иметь более общее решение, вы можете написать общий класс 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++;
});

(это вариант решения, данного Альмиром Кампосом).

В конкретном случае это не очень хорошее решение, так как Integer для вашей цели хуже, чем int, в любом случае, я думаю, что это решение более общее.