Страницы

Поиск по вопросам

среда, 10 октября 2018 г.

Захват значений в лямбда-выражении

Лямбда-выражения должны использоваться для захвата значений, а не переменных. Захват значений побуждает писать код без побочных эффектов, поскольку альтернатива труднее.
Что понимается под захватом значений в лямбда-выражении?


Ответ

Это значит, что в лямбда-выражениях стоит использовать внешние (относительно выражения) неизменяемые значения, а не внешние переменные, значение и внутреннее состояние которых могут меняться. Под внешними неизменяемыми значениями, соответственно, подразумеваются effectively final локальные переменные и поля примитивных типов, а также effectively final объекты, внутреннее состояние которых не будет меняться.
Связано это с тем, что Streams и лямбда-выражения проектировались из расчета на их многопоточное использование.
Проблема с использованием переменной (counter) вместо значения видна в таком примере:
private static class Element { private final int value;
public Element(int value) { this.value = value; }
public int getValue() { return value; } }
private static volatile int counter = 0;
public static void main(String[] args) { List list = new ArrayList<>(); for (int i = 0; i < 100 * 1000; i++) { list.add(new Element(1)); } list.parallelStream().forEach(e -> counter += e.getValue()); System.out.println(counter); }
Рассчитывать на то, что на экран будет выведено значение 100000, не приходится, потому что налицо race condition. В моём тесте этот код смог получить правильное значение только в 299 случаях из 100 тысяч.
Это одна из причин почему локальные переменные, используемые в лямбда-выражении, должны быть effectively final. Допустимость кода
int localCounter = 0; list.parallelStream().forEach(e -> localCounter += e.getValue());
Привела бы к race condition для локальной переменной, что стало бы новым витком проблем в многопоточном программировании на Java. Локальные переменные считаются потокобезопасными, и ломать этот принцип разработчикам Java не хотелось.
Можно "обдурить" компилятор в плане ограничения на effectively final значение таким образом:
int[] localCounter = { 0 }; list.parallelStream().forEach(e -> localCounter[0] += e.getValue()); System.out.println(localCounter[0]);
Так что "выстрелить себе в ногу" при использовании effectively final локальной переменной всё же можно. Конечно, не стоит удивляться тому, что значение опять-таки будет посчитано неправильно. На практике так делать определённо не стоит.
Да, здесь можно использовать AtomicInteger
AtomicInteger atomicInteger = new AtomicInteger(); list.parallelStream().forEach(e -> atomicInteger.addAndGet(e.getValue())); System.out.println(atomicInteger.get());
Однако это убивает всю идею распараллеливания кода.
В данном случае предполагается использование связки из map и reduce
int localCounter = list.parallelStream().map(e -> e.getValue()).reduce(0, (a, b) -> a + b); System.out.println(localCounter);
Часть с map и reduce можно записать и так:
.map(Element::getValue).reduce(0, Integer::sum)
Статью Brian Goetz (автора книги "Java Concurrency in Practice") по этому поводу можно прочитать здесь

Однако проблемы при захвате переменных вместо значений могут возникать не только при параллельном выполнении. Например:
private static class Element { public int x;
public Element(int x) { this.x = x; }
public Function getMapper() { return (e -> e + x); } }
public static void main(String[] args) { Element element = new Element(2); List list1 = Arrays.asList(10, 20, 30); Function function1 = element.getMapper(); element.x = 4; List list2 = Arrays.asList(10, 20, 30); Function function2 = element.getMapper(); list1 = list1.stream().map(function1).collect(Collectors.toList()); list2 = list2.stream().map(function2).collect(Collectors.toList()); System.out.println(list1); System.out.println(list2); }
В данном коде происходит захват переменной (не effectively final поля) x, из-за чего вместо ожидаемого вывода
[12, 22, 32] [14, 24, 34]
будет выведено
[14, 24, 34] [14, 24, 34]
При захвате же значения:
public Function getMapper() { int n = x; return (e -> e + n); }
такой проблемы/ошибки не возникнет.

Комментариев нет:

Отправить комментарий