Модифікація локальної змінної зсередини лямбда


115

Змінення локальної змінної в forEachдає помилку компіляції:

Нормальний

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

З Лямбдою

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

Будь-яка ідея, як це вирішити?


8
Зважаючи на те, що лямбда - це синтаксичний цукор для анонімного внутрішнього класу, моя інтуїція полягає в тому, що неможливо захопити не остаточну локальну змінну. Хоча я хотів би бути неправдивим.
Sinkingpoint

2
Змінна, що використовується в лямбда-виразі, повинна бути ефективно остаточною. Ви можете використовувати атомне ціле число, хоча воно є надмірним, тому лямбда-вираз тут не дуже потрібен. Просто приклейте петлю for.
Олексій К.

3
Змінна повинна бути фактично остаточною . Дивіться це: Чому обмеження на захоплення локальної змінної?
Джеспер


2
@Quirliom Вони не є синтаксичним цукром для анонімних занять. Лямбди використовують метод ручок під кришкою
Діоксин

Відповіді:


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або тип масиву цього типу. Наприклад, якщо ви використовуєте 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 масиви передаються посиланням. Коли ви переходите в масив, ви фактично передаєте адресу пам'яті для цього масиву. Примітивні типи, як ціле число, надсилаються за значенням, а це означає, що копія цього значення передається. Pass-by-value між початковим значенням та копією, що надсилається у ваші методи, не має відношення - маніпулювання копією нічого не відповідає оригіналу. Передача посилань означає, що вихідне значення і те, що відправлено в метод, є одне і те ж - маніпулювання ним у вашому методі змінить значення поза методом.
детективу

@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списку
Жека Козлов

Мені було б цікаво дізнатись, чи можна проблему прогресування повідомлення кожного 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++;
    }
});

1
... а що, якщо вам потрібно насправді прочитати результат? Результат не видно коду на верхньому рівні.
Люк Ушервуд

@LukeUsherwood: Ти маєш рацію. Це лише в тому випадку, якщо вам потрібно лише передавати дані ззовні в лямбда, а не виводити їх. Якщо вам потрібно вийти з нього, вам потрібно буде мати лямбда-фіксатор посилання на об'єкт, що змінюється, наприклад, на масив або об’єкт із не остаточними загальнодоступними полями, і передавати дані, встановлюючи їх в об'єкт.
newacct


3

Якщо ви перебуваєте на Java 10, ви можете використовувати varдля цього:

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

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 (), може працювати лише через -ефекти; їх слід використовувати обережно


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для ваших цілей, так чи інакше це рішення є більш загальним, я думаю.


0

Так, ви можете змінювати локальні змінні зсередини лямбда (як показано в інших відповідях), але робити це не слід. Лямбди призначені для функціонального стилю програмування, і це означає: Відсутність побічних ефектів. Те, що ви хочете зробити, вважається поганим стилем. Це також небезпечно у разі паралельних потоків.

Вам слід або знайти рішення без побічних ефектів, або використовувати традиційне для циклу.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.