Эта статья, как и все последующие и предыдущие – моя попытка структурировать полученные знания в процессе изучения Java. Здесь тезисно собрана вся основная информация по теме и те формулировки, которые показались мне наиболее удачными и понятными. Это мой конспект, если хотите.

Статья будет полезна тем, кто изучает или повторяет основы Java Core.
И тем, кто готовится к
техническому интервью.

На источники, откуда черпалась информация, предоставлены ссылки в конце статьи.

Оглавление

Основные понятия

С выходом Java 8 появилась поддержка функционального программирования.
Стало возможным конструировать сложные цепочки операций над потоком данных.

Функциона́льное программи́рование — парадигма программирования, в которой процесс вычисления трактуется как вычисление значений функций в математическом понимании последних (в отличие от функций как подпрограмм в процедурном программировании).

Пакет java.util.stream содержит классы для поддержки операций с потоками элементов в функциональном стиле. Ключевой абстракцией, введенной в этом пакете, является Поток.

Stream API – по сути это поток данных и последовательные операции над ними.

Интерфейсы Stream, IntStream, LongStream и DoubleStream – это потоки объектов и примитивных типов int, long и double.

Интерфейс Stream<T>

public interface Stream<T> extends BaseStream<T,Stream<T>>

Интерфейс Stream представляет собой последовательность элементов, поддерживающих последовательные и параллельные агрегатные операции.

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

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

Стрим примитивных типов

Чтобы обеспечить способ работы с тремя наиболее часто используемыми типами примитивов – int, long и double – библиотека java.util.stream включает три реализации стрима примитивов:

IntStream

public interface IntStream extends BaseStream<Integer,IntStream>

LongStream

public interface LongStream extends BaseStream<Long,LongStream>

DoubleStream

public interface DoubleStream extends BaseStream<Double,DoubleStream>

Создание потока примитивных типов mapToInt(), mapToDouble(), mapToLong()

List<Integer> numbers = new ArrayList<>();

numbers.stream().mapToInt(value -> value);

numbers.stream().mapToDouble(value -> value);
        
numbers.stream().mapToLong(value -> value);

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

Методы диапазона доступны только для IntStream и LongStream.
Существует два метода получения стрима из диапазона чисел:

range(int startInclusive, int endExclusive)

rangeClosed(int startInclusive, int endInclusive)

В методе range()второй аргумент метода не входит в диапазон чисел, в то время как метод rangeClosed() включает его в диапазон. 

Запустить Stream

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

Stream из List

List<String> list = new ArrayList<>();

list.stream();

list.parallelStream();           // параллельный поток

Stream из Map

Map<String, String> map = new HashMap<>();

map.entrySet().stream();

map.values().stream();

Stream из массива, используя статический метод класса Arrays

String[] array = new String[10];

Arrays.stream(array);

Stream из элементов, используя статические методы Классов-потоков

Stream.of("a", "b", "c");        // поток из элементов

Stream.of(array);                // поток из элементов массива

Stream.of(list);                 // поток из элементов списка List

Stream.generate(Math::random);   // генерация потока рандомных чисел

Stream.concat(stream1, stream2); // объединяет два потока в один

IntStream.range(1, 10);          // поток диапазона чисел от 1 до 9

IntStream.rangeClosed(1, 10);    // поток диапазона чисел от 1 до 10

Stream из строк буфера BufferedReader

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));

bufferedReader.lines();

Stream из строк файла через статический метод класса Files

Path path = Path.of("/root/test.txt");

Files.lines(path);

Stream из случайных чисел Random

Random random = new Random();

random.ints();

random.longs();

random.doubles();

Операции над Stream

Операции над потоком элементов бывают:

  • Intermediate – Промежуточные
    Не меняют данные, а только задают логику их изменения.
    С их помощью можно составить последовательную цепь операций над элементами.

  • Terminal – Конечные
    Запускают всю цепь промежуточных операций, закрывают поток и возвращают модифицированные данные.

Промежуточные операции НЕ выполняются без терминальных.
После обработки стрим нельзя повторно использовать.

Операции над потоком могут выполняться как последовательно, так и параллельно.
Чтобы превратить обычный стрим в параллельный, нужно вызвать промежуточный оператор parallel()

Stream<String> stream = list.stream();

stream.parallel();

Промежуточные операции. Intermediate

Данные операции удобно воспринимать как отложенные – они выполнятся когда их запустит конечная терминальная операция над стримом.

Провести операцию над каждым элементом peek()
Аналог forEach(), только промежуточный (нетерминальный)
Если в метод peek() передать функцию System.out::println, тогда все объекты будут выводиться на экран в момент, когда они будут проходить через поток.

Stream.of("a", "b", "c").peek(System.out::println);

Преобразовать данные из одного типа в другой map()
Можно передать функцию, которая преобразовывает один тип данных в другой.

Stream.of(1, 2, 3).map((x) -> String.valueOf(x));

Stream.of(1, 2, 3).map(String::valueOf);        // лямбда выражение

Stream.of("1", "2", "3").map(Integer::parseInt);

Отфильтровать элементы filter()

Stream.of(1, 2, 3, 4, 5).filter(n -> n < 4);    // [1, 2, 3]

Удалить дублирующиеся элементы distinct()

Stream.of(1, 2, 3, 2, 4, 2, 5).distinct();      // [1, 2, 3, 4, 5]

Сортировка и обратная сортировка элементов sorted()

Stream.of(4, 2, 3, 5, 1).sorted();              // [1, 2, 3, 4, 5]

Stream.of(4, 2, 3, 5, 1).sorted(Comparator.reverseOrder())

Лимит количества элементов limit()

Stream.of(1, 2, 3, 4, 5, 6).limit(3);            // [1, 2, 3]

Пропустить первые элементы skip()

Stream.of(1, 2, 3, 4, 5).skip(2);                // [3, 4, 5]

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

List<String> petNames = person.stream()
        .flatMap(person -> person.getPetName().stream())
        .collect(Collectors.toList());

System.out.println(petNames);             // [pet1, pet2, pet3, pet4, pet5]

Таким образом можно развернуть двухмерный массив

Integer[][] array2d = new Integer[][] {
                {1, 2, 3},
                {4, 5}
};

Arrays.stream(array2d).flatMap(Arrays::stream);    // [1, 2, 3, 4, 5]

Конечные операции. Terminal

Запускают всю цепь промежуточных операций и возвращают конечный результат, закрывают поток.

Собрать элементы потока и преобразовать их к нужному типу collect()
В аргумент метода нужно передать объект Collector.

Преобразовать поток в List Collectors.toList()

List<String> collect = Stream.of("a", "b", "c").collect(Collectors.toList());

Преобразовать поток в строку String Collectors.joining()

String collect = Stream.of("a", "b", "c").collect(Collectors.joining());

Итерация по каждому элементу forEach()

Stream.of("a", "b", "c").forEach(System.out::println);

Узнать количество элементов стрима count()

Stream.of("a", "b", "c").count();

Найти минимальное и максимальное значение min() и max()
Сравнение происходит с помощью объекта Comparator.
Возвращают объект класса Optional объект-контейнер, который может хранить null.
Метод get() –  возвращает значение, которое хранит объект Optional.

Optional<Integer> max = Stream.of(4, 2, 3, 5, 1)
        .max(Comparator.naturalOrder());

Integer maximum = max.get();

Integer minimum = Stream.of(4, 2, 3, 5, 1)
        .min(Comparator.naturalOrder())
        .get();

Comparator удобно задать с помощью лямбда-функции:

Stream.of("a", "bb", "ccc")
        .min((s1, s2) -> s1.length() - s2.length())
        .get();

Stream.of("a", "bb", "ccc")
        .max(Comparator.comparingInt(String::length))
        .get();

Найти первый подходящий элемент findFirst()
Возвращает первый подходящий элемент из стрима и завершается.
Возвращают объект класса Optional.

Stream.of(1, 2, 3, 4, 5)
        .filter(e -> e % 2 == 0)
        .findFirst()
        .get();

Найти любой подходящий элемент findAny()
Возвращает любой подходящий элемент из стрима и завершается.
Аналог метода findFirst()для потоков, которые обрабатываются параллельно.
Найденный элемент не обязательно будет первый по порядку в потоке.

Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .filter(e -> e % 2 == 0)
        .parallel()
        .findAny()
        .get();

Все элементы соответствуют условию allMatch()

Stream.of(1, 2, 3, 4, 5).allMatch(e -> e > 0);     // true

Все элементы НЕ соответствуют условию noneMatch()

Stream.of(1, 2, 3, 4, 5).noneMatch(e -> e > 0);    // false

Хотя бы один элемент соответствует условию anyMatch()

Stream.of(1, 2, 3, 4, 5).anyMatch(e -> e > 4);     // true

Сумма элементов стрима sum()
Это метод классов-стримов примитивных типов данных:
IntStream, LongStream и DoubleStream

List<Integer> integers = new ArrayList<>();

integers.stream()
        .mapToInt(i -> i)
        .sum();

Операция сведения Stream.reduce()

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

Участники операции сведения:

  • Identity - элемент, который является начальным значением операции сокращения и результатом по умолчанию, если поток пуст.

  • Accumulator - функция, которая принимает два параметра: частичный результат операции сведения и следующий элемент потока

  • Combiner - функция, используемая для объединения частичного результата операции сокращения и типами реализации аккумулятора.
    Если используем последовательные потоки, типы аргументов аккумулятора и типы его реализации не совпадают нужно использовать Combiner.

Сумма элементов списка

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

int sum = numbers.stream()
        .reduce(0, Integer::sum);

int sum2 = numbers.stream()
        .reduce(0, (subtotal, element) -> subtotal + element);

Сумма элементов в параллельном потоке
В таких случаях нужно использовать функцию для объединения результатов подпотоков в один – это роль Combiner Комбинатора.
В приведенном примере эту роль выполняет метод Integer::sum

int sum = numbers
        .parallelStream()
        .reduce(0, (a, b) -> a + b, Integer::sum);

int sum2 = ages
        .parallelStream()
        .reduce(0, Integer::sum, Integer::sum);

Объединение списка строк в одну сроку

List<String> letters = Arrays.asList("a", "b", "c", "d", "e");

String result = letters.stream()
        .reduce("", String::concat);

String result2 = letters.stream()
        .reduce("", (partialString, element) -> partialString + element);

Класс Optional

Методы интерфейса Stream findAny(), findFirst(), max(), min() и reduce() возвращают объект класса Optional

public final class Optional<T> extends Object

Это объект-контейнер, который может содержать или не содержать нулевое значение.
Ссылка на объект класса Optional может быть null
Если значение присутствует, метод isPresent() вернет true

Позволяет избавиться от проверки на null
Без этого класса приходилось писать проверку на NullPointerException.
Благодарю этому один объект Optional можно сравнить с другим объектом Optional через метод equals(), даже если они хранят в себе ссылки на null.

Получить элемент Optional get()
Метод класса Optoinal – возвращает значение, если оно присутствует, в противном случае бросит NoSuchElementException .

Stream.of("1", "22", "333")
        .max(Comparator.comparingInt(String::length))
        .get();

Класс Collectors

Метод collect() интерфейса Stream собирает данные в необходимую структуру данных, например в коллекции — List<T>Set<T>Map<T, R> 

public interface Collector<T,A,R>

В метод collect() в качестве параметра принимает объект типа Collector.
Статические методы класса Collectors возвращают такой объект класса Collector.

public final class Collectors extends Object

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

Преобразование потока в List toList()

list.stream().collect(Collectors.toList());

Преобразование потока в Set toSet()

list.stream().collect(Collectors.toSet());

Преобразование потока в Map toMap()

map.entrySet().stream()
        .map(e -> String.valueOf(e).split("="))
        .collect(Collectors.toMap(e -> e[0], e -> e[1]));

Объединение элементов в строку String joining()
Объединение потока коллекции List в одну строку через запятую

list.stream().collect(Collectors.joining(", "));

Сумма элементов потока summingInt(), summingDouble(), summingLong()
Например, сумма заработной платы сотрудников

List<Employee> employees = new ArrayList<>()

employees.stream()
        .collect(Collectors.summingDouble(Employee::getSalary)));

Сгруппировать элементы по условию groupingBy()
Группировка людей по стране

List<Person> people = new ArrayList<>()

people.stream()
        .collect(Collectors.groupingBy(Person::getCountry));

Список ссылок на источники информации

Комментарии (13)


  1. BugM
    30.10.2022 18:53
    +7

    Да отстаньте вы от стримов. Им уже 8 лет. Все уже написано, все уже сказано. Качественной учебной литературы море и больше не надо.


    1. zahaand Автор
      30.10.2022 19:17
      -7

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


      1. BugM
        30.10.2022 19:58
        +6

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


        1. zahaand Автор
          30.10.2022 20:05

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

          Так в чем проблема со статьей? Вам это всё просто уже хорошо знакомо?
          Мне вот нет – я разобрался для себя и законспектировал.
          Проблема в чем? Какой Тикток?


          1. BugM
            30.10.2022 20:20
            +2

            Проблема в том что это уже есть. Гораздо лучше. В любом желаемом виде. Еще раз тоже самое не надо. Тем более что у вас есть фактические ошибки. Стрим примитивных типов у вас полностью некорректно описан. Боксинг и вот это вот все это важно.

            Если вам надо читать что-то базовое о стримах перед собеседованием, то у вас точно есть большие проблемы в базовых знаниях. И задача собеседующего найти их. Это уровень джуна наверно. А джуну лучше не обзорные статьи читать, а нормальные. Которые для обучения.


            1. zahaand Автор
              30.10.2022 20:42

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


    1. sshikov
      30.10.2022 19:18
      +6

      Да фиг бы с ним, что им уже 8 лет (на самом деле больше — тогда релизный цикл был длиннее, и прототипы с работающими стримами появились на несколько лет раньше). Просто не нужен никому такой убогий пересказ документации в стиле «мне Рабинович напел».

      Скажем, это вот что вообще за фигня?

      Развернуть двумерный массив flatMap()


      Какой еще двумерный массив, с какой бы стати?


      1. zahaand Автор
        30.10.2022 19:55

        Вот кстати конструктивное замечание.
        Значение метода действительно глубже, чем было описано – дополнил и поправил.
        Спасибо!


        1. sshikov
          30.10.2022 20:22
          +1

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

          summingInt(), summingDouble(), summingLong() — потому что это очевидно.

          В тоже время, toMap зачастую нужны третий и четвертый параметры — я бы сказал, что в половине случаев в моем проекте они заданы. А вы их опустили.

          Ну или вот:

          public final class Collectors extends Object

          Бессмысленный кусок текста. Зачем это? Вы уже рядом пояснили, что это класс с кучкой статических методов, возвращающих разные коллекторы. При этом формальное описание самого Collector опущено.

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


          1. zahaand Автор
            30.10.2022 21:20

            Я конечно же с уважением отношусь к вашему мнению, что это всё шпаргалки.
            Но в этих шпаргалках собраны ответы на все частые вопросы технического интервью по Стримам. И даже больше.
            Поэтому со «Спасибо, мы вам перезвоним» спешить давайте не будем.

            По замечанию по параметрам я подумаю на сколько это уместно в статье такого формата. Спасибо!


            1. sshikov
              30.10.2022 21:25
              +2

              >Поэтому со «Спасибо, мы вам перезвоним» спешить давайте не будем.
              Я не настаиваю, и тем более никто вам не запретит такие тексты писать. Просто я уже давно чаще провожу интервью с нанимающей стороны, и у меня именно такое мнение складывается от вот таких примерно ответов — когда некие факты зазубрили, а понимания не видно. Не, на уровень джуна — сойдет, скорее всего (точнее, будет зависеть от ответов на уточняющие вопросы).

              Вот более-менее хорошие шпаргалки, как по мне — это www.baeldung.com, например.


              1. zahaand Автор
                30.10.2022 22:02

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

                Поэтому и назвал ее джава джуниор.

                Зубрить это точно никого не призываю. Это всё нужно понимать.
                А написать об этом – это и есть мой способ самому понять и разобраться.
                К критике отношусь адекватно и все замечания с радостью дополнительно прорабатываю – мне это только лучше помогает разобраться.

                И за ссылку – отдельное спасибо, добавил в закладки!


                1. sshikov
                  30.10.2022 22:15
                  +1

                  >Зубрить это точно никого не призываю.
                  Ну, по факту — а что можно сделать на базе этого текста? Вы же теорию не рассказываете, вы предлагаете именно избранные факты, которые только и можно что выучить/вспомнить. Как по мне, это и есть основной недостаток текстов подобного типа — они дают знания, но не понимание.

                  Скажем, вот есть такая штука как vavr.io. Там коллекции ведут себя примерно так:

                  List.of(1, 2, 3).map(e->e+1)… и, и все. И на выходе снова List, над которым применили функцию. Никакого .stream(), никакого .collect(), все просто и очевидно. Просто напрашивается вопрос, а надо ли сегодня вообще изучать стримы в том виде, как они появились в Java 8? Я вот не уверен (хотя например параллельных стримов vavr вроде не предлагает), при этом я часто жалею, что мы у себя в проекте полностью на них не перешли до сих пор.

                  P.S. На самом деле учить вот таким базовым вещам очень сложно.