Когда бы вы использовали collect()
vs reduce()
? Есть ли у кого-нибудь хорошие конкретные примеры, когда однозначно лучше пойти тем или другим путем?
Javadoc упоминает, что collect () является изменяемая редукция .
Учитывая, что это изменяемое сокращение, я предполагаю, что для этого требуется синхронизация (внутренняя), которая, в свою очередь, может отрицательно сказаться на производительности. Предположительно, reduce()
его легче распараллеливать за счет создания новой структуры данных для возврата после каждого шага сокращения.
Однако приведенные выше утверждения являются предположениями, и я хотел бы, чтобы здесь вмешался эксперт.
java
java-8
java-stream
jimhooker2002
источник
источник
Ответы:
reduce
является операцией « сворачивания », она применяет бинарный оператор к каждому элементу в потоке, где первый аргумент оператора - это возвращаемое значение предыдущего приложения, а второй аргумент - текущий элемент потока.collect
- это операция агрегирования, при которой создается «коллекция», и каждый элемент «добавляется» к этой коллекции. Затем коллекции в разных частях потока складываются.Документ , который вы связаны дает причину , имеющий два различных подхода:
reduce
Дело в том, что распараллеливание одинаково в обоих случаях, но в этом случае мы применяем функцию к самим элементам потока. В этомcollect
случае мы применяем функцию к изменяемому контейнеру.источник
int
является неизменным, поэтому вы не можете легко использовать операцию сбора. Вы могли бы сделать грязный хак, например использоватьAtomicInteger
или какой-нибудь обычай,IntWrapper
но зачем вам это? Операция складывания просто отличается от операции сбора.reduce
метод, с помощью которого вы можете возвращать объекты типа, отличного от элементов потока.Причина проста в том, что:
collect()
может работать только с изменяемыми объектами результата.reduce()
будет предназначен для работы с неизменяемыми объектами результата."
reduce()
с неизменным" примеромpublic class Employee { private Integer salary; public Employee(String aSalary){ this.salary = new Integer(aSalary); } public Integer getSalary(){ return this.salary; } } @Test public void testReduceWithImmutable(){ List<Employee> list = new LinkedList<>(); list.add(new Employee("1")); list.add(new Employee("2")); list.add(new Employee("3")); Integer sum = list .stream() .map(Employee::getSalary) .reduce(0, (Integer a, Integer b) -> Integer.sum(a, b)); assertEquals(Integer.valueOf(6), sum); }
collect()
пример " с изменяемым"Например , если вы хотите вручную вычислить сумму , используя
collect()
это не может работать с ,BigDecimal
но толькоMutableInt
изorg.apache.commons.lang.mutable
, например. Увидеть:public class Employee { private MutableInt salary; public Employee(String aSalary){ this.salary = new MutableInt(aSalary); } public MutableInt getSalary(){ return this.salary; } } @Test public void testCollectWithMutable(){ List<Employee> list = new LinkedList<>(); list.add(new Employee("1")); list.add(new Employee("2")); MutableInt sum = list.stream().collect( MutableInt::new, (MutableInt container, Employee employee) -> container.add(employee.getSalary().intValue()) , MutableInt::add); assertEquals(new MutableInt(3), sum); }
Это работает, потому что аккумулятор
container.add(employee.getSalary().intValue());
не должен возвращать новый объект с результатом, а должен изменять состояние изменяемогоcontainer
типаMutableInt
.Если вы хотите использовать
BigDecimal
вместо этого,container
вы не можете использовать этотcollect()
метод, посколькуcontainer.add(employee.getSalary());
не изменили бы,container
потому чтоBigDecimal
он неизменяемый. (Кроме этогоBigDecimal::new
не будет работать, так какBigDecimal
нет пустого конструктора)источник
Integer
конструктор (new Integer(6)
), который не рекомендуется в более поздних версиях Java.Integer.valueOf(6)
StringBuilder
изменяемый. См .: hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/…Нормальное сокращение предназначено для объединения двух неизменяемых значений, таких как int, double и т. Д., И создания нового; это неизменное сокращение. Напротив, метод collect предназначен для изменения контейнера для накопления результата, который он должен произвести.
Чтобы проиллюстрировать проблему, предположим, что вы хотите добиться
Collectors.toList()
простого сокращения, напримерList<Integer> numbers = stream.reduce( new ArrayList<Integer>(), (List<Integer> l, Integer e) -> { l.add(e); return l; }, (List<Integer> l1, List<Integer> l2) -> { l1.addAll(l2); return l1; });
Это эквивалент
Collectors.toList()
. Однако в этом случае вы изменяете файлList<Integer>
. Как мы знаем,ArrayList
он не является потокобезопасным и небезопасно добавлять / удалять из него значения во время итерации, поэтому вы получите либо одновременное исключение,ArrayIndexOutOfBoundsException
либо какое-либо исключение (особенно при параллельном запуске) при обновлении списка или объединителя. пытается объединить списки, потому что вы изменяете список, накапливая (добавляя) к нему целые числа. Если вы хотите сделать этот потокобезопасным, вам нужно каждый раз передавать новый список, что снизит производительность.Напротив,
Collectors.toList()
работает аналогичным образом. Однако он гарантирует безопасность потоков, когда вы накапливаете значения в списке. Из документации кcollect
методу :Итак, чтобы ответить на ваш вопрос:
если у вас есть незыблемые ценности , такие как
ints
,doubles
,Strings
то нормальное снижение работает просто отлично. Однако, если вам нужноreduce
преобразовать свои значения в, скажем,List
(изменяемую структуру данных), вам необходимо использовать изменяемое сокращение с помощью этогоcollect
метода.источник
x
потоки, каждый «добавляя к идентификатору», а затем объединяя их вместе. Хороший пример.public static void main(String[] args) { List<Integer> l = new ArrayList<>(); l.add(1); l.add(10); l.add(3); l.add(-3); l.add(-4); List<Integer> numbers = l.stream().reduce( new ArrayList<Integer>(), (List<Integer> l2, Integer e) -> { l2.add(e); return l2; }, (List<Integer> l1, List<Integer> l2) -> { l1.addAll(l2); return l1; });for(Integer i:numbers)System.out.println(i); } }
Я попробовал и не получил исключениеПусть поток будет a <- b <- c <- d
В сокращении,
у вас будет ((a # b) # c) # d
где # - это интересная операция, которую вы хотели бы проделать.
В коллекции,
у вашего коллектора будет какая-то собирающая структура К.
K потребляет. Затем K потребляет b. Затем K потребляет c. Затем K потребляет d.
В конце вы спрашиваете K, каков окончательный результат.
Затем К. дает его вам.
источник
Они сильно различаются по потенциальному объему памяти во время выполнения. Пока
collect()
собирает и помещает все данные в коллекцию,reduce()
явно просит вас указать, как уменьшить данные, прошедшие через поток.Например, если вы хотите прочитать некоторые данные из файла, обработать их и поместить в некоторую базу данных, вы можете получить код потока Java, подобный этому:
В этом случае мы используем,
collect()
чтобы заставить Java передавать данные и сохранять результат в базе данных. Безcollect()
данных никогда не читаются и никогда не сохраняются.Этот код успешно генерирует
java.lang.OutOfMemoryError: Java heap space
ошибку времени выполнения, если размер файла достаточно велик или размер кучи достаточно мал. Очевидная причина в том, что он пытается сложить все данные, которые прошли через поток (и, по сути, уже были сохранены в базе данных), в результирующую коллекцию, и это взорвёт кучу.Однако, если вы замените его
collect()
наreduce()
- это больше не будет проблемой, поскольку последний уменьшит и отбросит все данные, которые прошли.В представленном примере просто замените
collect()
что-нибудь наreduce
:.reduce(0L, (aLong, result) -> aLong, (aLong1, aLong2) -> aLong1);
Вам даже не нужно заботиться о том, чтобы вычисления зависели от того,
result
что Java не является чистым языком FP (функционального программирования) и не может оптимизировать данные, которые не используются в нижней части потока из-за возможных побочных эффектов. .источник
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7); int sum = list.stream().reduce((x,y) -> { System.out.println(String.format("x=%d,y=%d",x,y)); return (x + y); }).get();
System.out.println (сумма);
x=1,y=2 x=3,y=3 x=6,y=4 x=10,y=5 x=15,y=6 x=21,y=7 28
Уменьшите дескриптор функции двух параметров, первый параметр - это предыдущее возвращаемое значение в потоке, второй параметр - это текущее вычисляемое значение в потоке, он суммирует первое значение и текущее значение как первое значение в следующем вычислении.
источник
Согласно документам
Так что в основном вы будете использовать
reducing()
только в случае принудительного сбора. Вот еще один пример :For example, given a stream of Person, to calculate the longest last name of residents in each city: Comparator<String> byLength = Comparator.comparing(String::length); Map<String, String> longestLastNameByCity = personList.stream().collect(groupingBy(Person::getCity, reducing("", Person::getLastName, BinaryOperator.maxBy(byLength))));
Согласно этому руководству, сокращение иногда менее эффективно
Таким образом, удостоверение «повторно используется» в сценарии сокращения, поэтому использовать его будет немного эффективнее,
.reduce
если это возможно.источник