Страницы

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

четверг, 5 декабря 2019 г.

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

#java #лямбда_выражение



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


Что понимается под захватом значений в лямбда-выражении?
    


Ответы

Ответ 1



Это значит, что в лямбда-выражениях стоит использовать внешние (относительно выражения) неизменяемые значения, а не внешние переменные, значение и внутреннее состояние которых могут меняться. Под внешними неизменяемыми значениями, соответственно, подразумеваются 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); } такой проблемы/ошибки не возникнет.

Ответ 2



Наверное, автор имел ввиду, что лямбда-выражения должны трогать не переменные, а принимать на вход значение и отдавать значения на выходе. Например: Плохо: final String string = "string"; class.method(() -> string += "abc"); Хорошо: class.method((string) -> string += "abc");

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

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