Лямбда-выражения должны использоваться для захвата значений, а не переменных. Захват значений побуждает писать код без побочных эффектов, поскольку альтернатива труднее.
Что понимается под захватом значений в лямбда-выражении?
Ответ
Это значит, что в лямбда-выражениях стоит использовать внешние (относительно выражения) неизменяемые значения, а не внешние переменные, значение и внутреннее состояние которых могут меняться. Под внешними неизменяемыми значениями, соответственно, подразумеваются 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
Рассчитывать на то, что на экран будет выведено значение 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
public static void main(String[] args)
{
Element element = new Element(2);
List
В данном коде происходит захват переменной (не effectively final поля) x, из-за чего вместо ожидаемого вывода
[12, 22, 32]
[14, 24, 34]
будет выведено
[14, 24, 34]
[14, 24, 34]
При захвате же значения:
public Function
такой проблемы/ошибки не возникнет.
Комментариев нет:
Отправить комментарий