Сокращение, группировка и разбиение на разделы, подробное объяснение операций финализации JavaStream.

Java задняя часть
Сокращение, группировка и разбиение на разделы, подробное объяснение операций финализации JavaStream.

Эта статья является первой подписанной статьей сообщества Nuggets, и ее перепечатка без разрешения запрещена.

Градостроительство ментальной карты, в первую очередь спасибо за положительную поддержку моей прошлой статьи и помощь в выполнении KPI😄.

ПредыдущийВ этой главе я рассказал вам о первой половине Stream, включая общий обзор Stream, создание Stream и операцию преобразования потока Stream, а также краткое описание некоторых моментов внутренней оптимизации Stream.

Хотя уже поздно, сегодня я продолжу информировать вас о второй части знаний о Stream - конечной операции, Поскольку содержание API этой части многочисленно и сложно, я открою единственную статью, чтобы рассказать вам об этом в подробно Моя статья очень длинная, пожалуйста, потерпите меня.

Перед официальным стартом поговорим о характеристиках самого метода агрегации (я буду использовать метод агрегации для обозначения метода в операции финализации):

  1. Метод агрегации представляет окончательный результат вычисления всего потока, поэтому его возвращаемое значение не является потоком.

  2. Возвращаемое значение метода агрегации может быть пустым.Например, если фильтр не соответствует, в JDK8 используется Optional, чтобы избежать NPE.

  3. Методы агрегации будут вызывать метод оценки, который является внутренним методом, который можно использовать для определения того, является ли метод методом агрегации в процессе просмотра исходного кода.

хорошо, зная характеристики методов агрегации, я разделил методы агрегации на несколько категорий для простоты понимания:

Среди них я кратко объясню простой метод агрегации, а остальные сосредоточатся на объяснении, особенно сборщик, который может сделать слишком много. . .

Метод агрегации Stream является обязательной операцией в нашем использовании Stream.Если вы внимательно изучите эту статью, вы сразу же сможете освоиться со Stream, по крайней мере, вы можете это сделать 😄

1. Простой метод агрегирования

Первый раздел, давайте начнем с чего-то простого.

Stream имеет больше методов агрегации, чем методы без сохранения состояния и с сохранением состояния, упомянутые в предыдущей статье, но некоторые из них можно изучить с первого взгляда.В первом разделе давайте сначала поговорим об этих методах:

  • count(): возвращает размер элементов в потоке.

  • forEach(): потреблять каждый элемент через все элементы во внутреннем цикле Stream, этот метод не имеет возвращаемого значения.

  • forEachOrder(): Тот же эффект, что и у описанного выше метода, но при этом можно поддерживать порядок потребления даже в многопоточной среде.

  • anyMatch(Predicate predicate): это операция короткого замыкания, когда передается параметр утверждения, чтобы определить, существует ли элемент, который может соответствовать утверждению.

  • allMatch(Predicate predicate): это операция короткого замыкания, которая возвращает, могут ли все элементы соответствовать утверждению, передавая параметр утверждения.

  • noneMatch(Predicate predicate): это операция короткого замыкания, путем передачи параметра утверждения, чтобы определить, не могут ли все элементы соответствовать утверждению, если да, вернуть true, иначе false.

  • findFirst(): это операция короткого замыкания, возвращающая первый элемент в потоке, поток может быть пустым, поэтому возвращаемое значение обрабатывается с опциональным.

  • findAny(): это операция короткого замыкания, возвращающая любой элемент в потоке, который обычно является первым элементом в потоке строк, и поток может быть пустым, поэтому возвращаемое значение обрабатывается опциональным.

Хотя вышеперечисленное относительно просто, есть пять методов, включающих операции короткого замыкания, которые я хотел бы упомянуть:

прежде всегоfindFirst()иfindAny()Эти два метода, поскольку им нужно получить только один элемент для завершения метода, эффект короткого замыкания хорошо понятен.

с последующимanyMatchметода, ему нужно только сопоставить метод элемента, чтобы закончить, поэтому его эффект короткого замыкания также хорошо понятен.

Ну наконец тоallMatchМетоды иnoneMatchНа первый взгляд, эти два метода должны пройти через все элементы во всем потоке, но это не так.Например, allMatch может возвращать false, пока один элемент не соответствует утверждению, а noneMatch может утверждать, что пока один совпадения элементов возвращают false, поэтому все они являются методами с эффектом короткого замыкания.

2. Сокращение

2.1 уменьшить: итеративная оценка

Во втором разделе поговорим о редукции. Поскольку слово слишком абстрактное, мне пришлось найти простое для понимания объяснение, чтобы перевести это предложение. Ниже приводится определение редукции:

Повторное объединение всех элементов в потоке для получения результата называется сокращением.

Примечание. В функциональном программировании это называется сверткой.

Чтобы привести очень простой пример, у меня есть три элемента, 1, 2 и 3. Я складываю их вместе и, наконец, получаю число 6. Этот процесс и есть сокращение.

Другой пример: у меня есть три элемента: 1, 2 и 3. Я сравниваю их и, наконец, выбираю наибольшее число 3 или наименьшее число 1. Этот процесс также является редукцией.

Ниже я привожу пример суммирования для демонстрации редукции, в котором используется метод редукции:

        Optional<Integer> reduce = List.of(1, 2, 3).stream()
                .reduce((i1, i2) -> i1 + i2);

Во-первых, вы, возможно, заметили, что я использовал слово лян в приведенных выше небольших примерах, что означает, что сокращение заключается в обработке элементов ляна и получении конечного значения, поэтому параметр метода сокращения является двоичным выражения, он произвольно обрабатывает два параметра и, наконец, получает результат, в котором его параметры и результат должны быть одного типа.

Например, в коде i1 и i2 являются двумя параметрами двоичного выражения, они представляют первый элемент и второй элемент в элементе соответственно.Когда первое добавление будет завершено, результат будет присвоен i1 в теле , i2 будет продолжать представлять следующий элемент до тех пор, пока элемент не будет исчерпан и не будет получен окончательный результат.

Если вы считаете, что это недостаточно элегантно, вы также можете использовать метод по умолчанию в Integer:

        Optional<Integer> reduce = List.of(1, 2, 3).stream()
                .reduce(Integer::sum);

Это также方法引用Представляет пример лямбда-выражения.

Возможно, вы также заметили, что их возвращаемое значение является необязательным, на случай, если в Stream нет элементов.

Также можно найти способ избавиться от этой ситуации, то есть пусть у элемента будет хотя бы одно значение, здесь reduce предоставляет нам перегруженный метод:

        Integer reduce = List.of(1, 2, 3).stream()
                .reduce(0, (i1, i2) -> i1 + i2);

В приведенном выше примере перед двоичным выражением добавляется дополнительный параметр, который называется начальным значением, так что даже если в вашем потоке нет элементов, он в конечном итоге вернет 0, поэтому необязательный параметр не нужен.

В реальном методе начальное значение будет занимать позицию I1 при первом выполнении, i2 представляет первый элемент в потоке, затем результирующее и повторно занимающее позицию I1, i2 представляет следующий элемент.

Однако использование начального значения не обходится без затрат, оно должно соответствовать принципу:accumulator.apply(identity, i1) == i1, то есть, когда он выполняется в первый раз, его возвращаемый результат должен быть первым элементом в вашем потоке.

Например, мой пример выше — это операция сложения, тогда первое сложение — это0 + 1 = 1, в соответствии с вышеуказанным принципом, этот принцип должен гарантировать, что правильный результат может быть получен в случае параллельного потока.

Если ваше начальное значение равно 1, то каждый поток инициализируется равным 1 при параллелизме, и ваша окончательная сумма будет больше, чем вы ожидали.

2.2 max: используйте уменьшение, чтобы найти максимум

Метод max также является методом сокращения, который напрямую вызывает метод сокращения.

Сначала рассмотрим пример:

        Optional<Integer> max = List.of(1, 2, 3).stream()
                .max((a, b) -> {
                    if (a > b) {
                        return 1;
                    } else {
                        return -1; 
                    }
                });

Да, именно так используется метод max, что заставляет меня думать, что я не использую функциональный интерфейс.Конечно, вы также можете использовать метод Integer для упрощения:

        Optional<Integer> max = List.of(1, 2, 3).stream()
                .max(Integer::compare);

Несмотря на это, этот метод все еще заставляет меня чувствовать себя очень громоздким.Хотя я понимаю, что передача параметров в метод max позволяет нам настраивать правила сортировки, я не понимаю, почему нет метода по умолчанию для сортировки в соответствии с естественной сортировкой , и Нужно ли мне передавать параметры.

Позже я подумал о базовом типе Stream, и, конечно же, они могут получить максимальное значение напрямую, без передачи параметров:

        OptionalLong max = LongStream.of(1, 2, 3).max();

Разумеется, все, что я могу придумать, придумали дизайнеры библиотеки классов~

Примечание: OptionalLong — это инкапсуляция базового типа long с помощью Optional.

2,3 мин: используйте сокращение, чтобы найти минимум

Давайте посмотрим непосредственно на пример:

        Optional<Integer> max = List.of(1, 2, 3).stream()
                .min(Integer::compare);

Отличие его от max в том, что нижний слой>заменяется<, слишком упрощенно и не будет повторяться здесь.

3. Коллектор

В третьем разделе рассмотрим сборщик, его функция состоит в том, чтобы собирать элементы в потоке для формирования новой коллекции.

Хотя я уже приводил ментальную карту в начале этой статьи, но так как API для сборщиков много, я нарисовал еще одну, которая является дополнением к той, что была в начале:

Имя метода сборщика — collect, а его метод определяется следующим образом:

    <R, A> R collect(Collector<? super T, A, R> collector);

Как следует из названия, сборщик используется для сбора элементов Stream, и мы можем настроить то, что будет собрано в итоге, но нам, как правило, не нужно писать это самим, потому что JDK имеет встроенный класс реализации Коллекционеров - Коллекторы.

3.1 Метод сбора

Через Collectors мы можем использовать его встроенные методы для простого сбора данных:

Например, если вы хотите собрать элементы в коллекцию, вы можете использовать методы toCollection или toList, но обычно мы не используем toCollection, потому что ему нужно передавать параметры, а передавать параметры никому не нравится.

Вы также можете использовать toUnmodifiableList, который отличается от toList тем, что возвращаемая им коллекция не может изменять элементы, например удалять или добавлять.

В другом примере, если вы хотите собирать элементы после дедупликации, вы можете использовать toSet или toUnmodifiableSet.

Вот более простой пример:

        // toList
        List.of(1, 2, 3).stream().collect(Collectors.toList());

        // toUnmodifiableList
        List.of(1, 2, 3).stream().collect(Collectors.toUnmodifiableList());

        // toSet
        List.of(1, 2, 3).stream().collect(Collectors.toSet());

        // toUnmodifiableSet
        List.of(1, 2, 3).stream().collect(Collectors.toUnmodifiableSet());

Все вышеперечисленные методы не имеют параметров и готовы к использованию, нижний слой toList также является классическим ArrayList, а нижний слой toSet — это классический HashSet.


Возможно, иногда вам может понадобиться собрать карту, например, путем преобразования данных заказа в номер заказа, соответствующий заказу, тогда вы можете использовать toMap():

        List<Order> orders = List.of(new Order(), new Order());

        Map<String, Order> map = orders.stream()
                .collect(Collectors.toMap(Order::getOrderNo, order -> order));

toMap() имеет два параметра:

  1. Первый параметр представляет собой ключ, что означает, что вы хотите установить ключ карты. Здесь я указываю номер заказа в элементе.

  2. Второй параметр представляет значение, что означает, что вы хотите установить значение карты. Я напрямую использую сам элемент в качестве значения, поэтому результатом является карта .

Вы также можете использовать атрибуты элементов в качестве значений:

        List<Order> orders = List.of(new Order(), new Order());

        Map<String, List<Item>> map = orders.stream()
                .collect(Collectors.toMap(Order::getOrderNo, Order::getItemList));

Это возвращает карту номера заказа + список элементов.

toMap() имеет два сопутствующих метода:

  • toUnmodifiedMap(): возвращает неизменяемую карту.

  • toConcurrentMap(): возвращает потокобезопасную карту.

Параметры этих двух методов точно такие же, как у toMap(), единственное отличие состоит в том, что характеристики карты, сгенерированные нижним слоем, не совпадают.Мы обычно используем простой toMap(), и его нижний слой является нашим наиболее часто используемым используется HashMap() выполнить.

Хотя функция toMap() является мощной и широко используемой, у нее есть фатальный недостаток.

Мы знаем, что HahsMap перезапишется, когда обнаружит тот же ключ, но если указанный вами ключ дублируется, когда метод toMap() генерирует карту, он сразу вызовет исключение.

Например, в приведенном выше примере заказа мы предполагаем, что номера заказов двух заказов одинаковы, но вы укажете номер заказа в качестве ключа, тогда метод напрямую выдаст исключение IllegalStateException, поскольку он не позволяет использовать ключи в элементы должны быть одинаковыми.

3.2 Метод группировки

Если вы хотите классифицировать данные, но указанный вами ключ является повторяемым, вам следует использовать groupingBy вместо toMap.

В качестве простого примера я хочу сгруппировать коллекцию заказов по типу заказа, поэтому я могу сделать это:

        List<Order> orders = List.of(new Order(), new Order());

        Map<Integer, List<Order>> collect = orders.stream()
                .collect(Collectors.groupingBy(Order::getOrderType));

Непосредственно укажите атрибут элемента, используемый для группировки, он будет автоматически группироваться в соответствии с этим атрибутом и собирать результаты группировки в виде списка.

        List<Order> orders = List.of(new Order(), new Order());

        Map<Integer, Set<Order>> collect = orders.stream()
                .collect(Collectors.groupingBy(Order::getOrderType, toSet()));

groupingBy также предоставляет перегрузку, позволяющую настраивать тип сборщика, поэтому его вторым параметром является объект сборщика Collector.

Для типа Collector мы обычно по-прежнему используем класс Collectors.Поскольку мы использовали Collectors раньше, нет необходимости объявлять и напрямую передавать метод toSet(), что означает, что мы собираем сгруппированные элементы как набор.

В groupingBy также есть аналогичный метод, называемый groupingByConcurrent(), который может повысить эффективность группировки при параллельном выполнении, но не гарантирует порядок, поэтому здесь он обсуждаться не будет.

3.3 Метод разделения

Далее я представлю еще один случай группировки — раздел, название немного сбивает с толку, но смысл очень прост:

将数据按照TRUE或者FALSE进行分组就叫做分区。

Например, мы группируем коллекцию заказов по тому, оплачены они или нет, это раздел:

        List<Order> orders = List.of(new Order(), new Order());
        
        Map<Boolean, List<Order>> collect = orders.stream()
                .collect(Collectors.partitioningBy(Order::getIsPaid));        

Так как оплачен заказ или нет имеет только два состояния: оплачен и неоплачен, такой способ группировки называется партицией.

Как и groupingBy, он также имеет перегруженный метод для настройки типа коллектора:

        List<Order> orders = List.of(new Order(), new Order());

        Map<Boolean, Set<Order>> collect = orders.stream()
                .collect(Collectors.partitioningBy(Order::getIsPaid, toSet()));

3.4 Классический метод повторной гравировки

Наконец-то добрался до последнего раздела, прошу меня простить за столь грязное название методам этой части, но эти методы именно то, что я сказал: классическая гравировка.

Другими словами, в Collectors снова реализованы исходные методы Stream, в том числе:

  1. mapmapping

  2. filterfiltering

  3. flatMapflatMapping

  4. countcounting

  5. reducereducing

  6. maxmaxBy

  7. **мин** →minBy

Я не буду перечислять функции этих методов по порядку, в предыдущих статьях они уже были подробно описаны, разница лишь в том, что некоторые методы имеют дополнительный параметр, это параметр коллекции, о котором мы говорили в разделе группировка и партиционирование. Вы можете указать, в какой контейнер собирать.

Я вынул их и в основном хотел сказать, почему существует так много способов борьбы с этим.Здесь я буду говорить о своих личных мнениях, а не официальных мнениях.

Я думаю, что это в основном для совмещения функций.

Что это обозначает? Допустим, у меня есть еще одна потребность: сгруппировать заказы по типу заказа и узнать, сколько заказов в каждой группе.

О группировке заказов мы уже говорили.Чтобы узнать, сколько заказов в каждой группе, нужно только получить размер соответствующего списка, но мы можем сделать это в один шаг без особых хлопот.При выводе результата, пара ключ-значение — это тип заказа и количество заказа:

        Map<Integer, Long> collect = orders.stream()
                .collect(Collectors.groupingBy(Order::getOrderType, counting()));

Все, все так просто, все, значит, мы выполнили еще одну счетную операцию над сгруппированными данными.

Приведенный выше пример может быть неочевидным.Когда нам нужно работать с данными после последней коллекции, нам обычно нужно повторно преобразовать их в поток, а затем обработать их, но использование этих методов Коллекторов может сделать это очень удобным для вас. в обработке данных коллекторов.

Для другого примера, мы по-прежнему группируем заказы по типу заказа, но мы хотим получить тот, у которого наибольшая сумма каждого типа заказа, тогда мы можем сделать это:

        List<Order> orders = List.of(new Order(), new Order());        
       
        Map<Integer, Optional<Order>> collect2 = orders.stream()
                .collect(groupingBy(Order::getOrderType, 
                        maxBy(Comparator.comparing(Order::getMoney))));

Это более лаконично и удобно, и нам не нужно идти искать максимальное значение по одному после того, как мы сделали группировку, которую можно сделать за один шаг.

После другой группы найдите сумму заказа каждой группы:

        List<Order> orders = List.of(new Order(), new Order());        
       
        Map<Integer, Long> collect = orders.stream()
                .collect(groupingBy(Order::getOrderType, summingLong(Order::getMoney)));

Однако summingLong здесь не упоминается — это встроенный запрос и операция, поддерживающая Integer, Long и Double.

Существует также аналогичный метод под названием averagingLong. Вы можете узнать это, взглянув на название. Найти среднее значение относительно просто. Рекомендуется взглянуть на него, когда вы в порядке.


Это конец, последний метод, joining(), очень полезен для объединения строк:

        List<Order> orders = List.of(new Order(), new Order());

        String collect = orders.stream()
                .map(Order::getOrderNo).collect(Collectors.joining(","));

Имя метода этого метода выглядит немного знакомым: да, класс String был добавлен после JDK8.join()Этот метод также используется для объединения строк.Соединение коллекторов такое же, как и его функция, и базовая реализация также такая же, с использованием класса StringJoiner.

4. Резюме

Наконец закончил писать.

В этой конечной операции в Stream я упомянул все методы агрегации в Stream.Можно сказать, что после прочтения этой статьи все операции агрегации Stream будут освоены.Неважно, если вы им не пользуетесь, вы знайте, что Вещи в порядке, иначе Stream вообще не сможет делать XX вещей в вашей системе знаний, и это немного нелепо.

Конечно, я по-прежнему рекомендую вам использовать эти краткие API в ваших проектах, чтобы улучшить читабельность кода и сделать его более кратким, и легко заставить других сиять, когда его рецензируют~

Увидев это, я надеюсь, что вы можете поставить мне палец вверх и внести свой вклад в мою карьеру KPI.Ваша поддержка является неисчерпаемой движущей силой для моего творчества.До встречи в следующем выпуске.


Справочная литература:

рекомендуемая статья: