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

115

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

Нормальный

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

С лямбдой

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

Есть идеи, как это решить?

Patan
источник
8
Учитывая, что лямбда-выражения - это, по сути, синтаксический сахар для анонимного внутреннего класса, моя интуиция подсказывает, что невозможно захватить не финальную локальную переменную. Однако я бы хотел, чтобы меня доказали, что я ошибаюсь.
Sinkingpoint
2
Переменная, используемая в лямбда-выражении, должна быть фактически окончательной. Вы можете использовать атомарное целое число, хотя это излишне, поэтому лямбда-выражение здесь не требуется. Просто придерживайтесь цикла for.
Alexis C.
3
Переменная должна быть окончательной . См. Это: Почему ограничение на захват локальных переменных?
Джеспер
2
@Quirliom Это не синтаксический сахар для анонимных классов. Лямбды используют ручку метода под капотом
Dioxin

Ответы:

177

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

Любая обертка хороша.

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

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

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

Конечно, это все еще верно для типов, отличных от int. Вам нужно только изменить тип упаковки AtomicReferenceна массив этого типа. Например, если вы используете a 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";
});
Оливье Грегуар
источник
Почему вам разрешено использовать массив вроде int[] ordinal = { 0 };? Вы можете это объяснить. Спасибо
mrbela
@mrbela Что именно ты не понимаешь? Может быть, я могу прояснить этот конкретный момент?
Оливье Грегуар
1
Массивы @mrbela передаются по ссылке. Когда вы передаете массив, вы фактически передаете адрес памяти для этого массива. Примитивные типы, такие как целое число, отправляются по значению, что означает, что передается копия значения. При передаче по значению нет никакой связи между исходным значением и копией, отправленной в ваши методы - манипулирование копией ничего не делает с оригиналом. Передача по ссылке означает, что исходное значение и значение, отправленное в метод, являются одним и тем же - манипулирование им в вашем методе изменит значение вне метода.
Детектив
@Olivier Grégoire, это действительно спасло мою шкуру. Я использовал решение Java 10 с «var». Вы можете объяснить, как это работает? Я везде гуглил, и это единственное место, которое я нашел с таким конкретным использованием. Я предполагаю, что, поскольку лямбда разрешает (эффективно) только конечные объекты за пределами своей области видимости, объект «var» в основном обманывает компилятор, заставляя его сделать вывод, что он окончательный, поскольку он предполагает, что только конечные объекты будут ссылаться вне области видимости. Не знаю, это мое лучшее предположение. Если вы хотите дать объяснение, я буду признателен, так как мне интересно знать, почему мой код работает :)
oaker
1
@oaker Это работает, потому что Java создаст класс MyClass $ 1 следующим образом: class MyClass$1 { int value; }В обычном классе это означает, что вы создаете класс с частной переменной пакета с именем value. Это то же самое, но на самом деле класс анонимен для нас, а не для компилятора или JVM. Java скомпилирует код, как если бы он был MyClass$1 wrapper = new MyClass$1();. И анонимный класс становится просто другим классом. В конце концов, мы просто добавили синтаксический сахар поверх него, чтобы сделать его читабельным. Кроме того, этот класс является внутренним классом с закрытым для пакета полем. Это поле можно использовать.
Оливье Грегуар,
14

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

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

IntStream.range(0, list.size())
         .forEach(i -> list.get(i).setOrdinal(i));
Стюарт Маркс
источник
3
Хорошее решение, но только если listбудет RandomAccessсписок
ЖекаКозлов 05
Мне было бы любопытно узнать, можно ли практически решить проблему сообщения о прогрессе каждые k итераций без выделенного счетчика, то есть в этом случае: stream.forEach (e -> {doSomething (e); if (++ ctr% 1000 = = 0) log.info ("Я обработал {} элементов", ctr);} Я не вижу более практичного способа (сокращение могло бы сделать это, но было бы более подробным, особенно с параллельными потоками).
zakmck
14

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

list.forEach(new Consumer<Example>() {
    int ordinal = 0;
    public void accept(Example s) {
        s.setOrdinal(ordinal);
        ordinal++;
    }
});
newacct
источник
1
... а что, если вам действительно нужно прочитать результат? Результат не виден для кода верхнего уровня.
Люк Ашервуд
@LukeUsherwood: Ты прав. Это только в том случае, если вам нужно только передать данные извне в лямбду, а не получать их. Если вам нужно получить это, вам нужно, чтобы лямбда захватывала ссылку на изменяемый объект, например массив или объект с неокончательными общедоступными полями, и передавала данные, устанавливая их в объект.
newacct
3

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

var ordinal = new Object() { int value; };
list.forEach(s -> {
    s.setOrdinal(ordinal.value);
    ordinal.value++;
});
ZhekaKozlov
источник
3

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

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

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

Закмц
источник
2

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

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

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

Альмир Кампос
источник
1

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

Процитировать javadoc

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

codemonkey
источник
0

У меня была немного другая проблема. Вместо увеличения локальной переменной в 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" окончательным, но я не получал ошибок компиляции, оставив эти детали как есть.

Стив Т
источник
0

Чтобы получить более общее решение, вы можете написать общий класс 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для вашей цели, в любом случае, я думаю, это решение является более общим.

luca.vercelli
источник
0

Да, вы можете изменять локальные переменные изнутри лямбда-выражений (как показано в других ответах), но вы не должны этого делать. Лямбды предназначены для функционального стиля программирования, а это означает: отсутствие побочных эффектов. То, что вы хотите делать, считается плохим стилем. Это также опасно в случае параллельных потоков.

Вы должны либо найти решение без побочных эффектов, либо использовать традиционный цикл for.

Донат
источник