StreamAPIExample

Учебный проект для изучения Stream API и закрепления навыков работы с ним. Подразумевает выполнение студентом указанных в коде задач в из //TODO описания.

Как пользоваться проектом

В классе Main находятся задания для выполнения, помечанные комментарием //TODO. На данный момент прописаны задачи для работы с коллекциями:

  • Cat - котики
  • Shawarma - шаурма

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

ArrayList<Cat> cats = new CatListGenerator().randomCats(1000);

Метод вернёт коллекцию с указанным количеством объектов класса Cat с которой можно будет выполнять дальнейшие операции.

Справка по LocalDateTime

Можно прочитать в выдержке из методички.

Справка по StreamAPI

StreamAPI позволяет оперировать с потоком данных (не путать с потоком выполнения) из массивов или коллекций, иными словами, из любого набора данных. Это можно сравнить с тем, как груз перемещается по реке из одного источника и при помощи специальных механизмов можно выбирать только нужные «ящики», переводить их в отдельные русла и собирать их в отдельный набор. Иными словами, StreamAPI придерживается следующей последовательности работы:

image

Разберём каждый блок из последовательности:

  • Источник – это набор данных, откуда изначально берутся элемен-ты из которых формируется поток данных для обработки;
  • Промежуточный оператор – это некоторый метод, который мо-жет принимать на вход анонимную (лямбда) функцию, описываю-щую какое именно действие надо произвести над каждым из элементов. Например, фильтрацию элементов по определённому условию. Все промежуточные операторы возвращают новый объект типа Stream;
  • Терминальный оператор – к нему относятся методы, которые либо ничего не возвращают (void), либо возвращают объекты других типов.

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

Более того, StreamAPI это реализация монады – паттерна функционального программирования, в котором происходит композиция (то есть вы-страивание в цепочки) действий, которые иначе были бы разделены строками небезопасного и избыточного кода.

Основные методы StreamAPI

.filter(Predicate<T> P)

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

.distinct() 

возвращает набор данных, исключая все дубликаты. Работает это следующим образом. По умолчанию, в каждом классе есть методы .equals(Object obj) и hashCode(), неявно наследуемые от суперкласса Object. Эти методы нужны для сравнения двух объектов между собой и по умолчанию сравниваются по значению их ссылок.

.min/.max(Comparator<? super T> c)

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

Comparator<? super T>

это ещё один функциональный интерфейс, предназначенный для сравнения объектов по значениям их свойств. Для примера, в описании интерфейса существует метод по умолчанию .comparingInt, который принимает целочисленные значения и использует статический метод .compare у класса Integer для сравнения двух чисел.

.map(Function<? super T, ? extends R> mapper)

хитрая функция, ко-торая применяет к каждому элементу набора функциональный интерфейс Function, который выполняет некоторое извлечение одного типа R из типа T. Название происходит от слова mapping или отображения некоторой функции на наборе данных. Этот термин синонимичен термину преобразования, но со смыслом «создания новой версии». Важно знать, что возвращаемым объектом всегда будет поток данных того типа, который обладает извлекаемое из объекта свойство.

.flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)

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

.peek(Consumer<? super T> consumer) 

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

.collect(Collector collector)

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

Рассмотрим некоторые доступные методы Collectors:

.toList()

собирает набор элементов в коллекцию типа список. Посколь-ку возвращается объект с типом – интерфейса List, его нужно явно преобразовать к необходимой реализации списка, например, к ArrayList.

.toSet() 

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

.toMap(Function<? super T, ? extends K> keyMapper, 
Function<? super T, ? extends U> valueMapper)

группирует элементы в словарь и принимает на вход два функциональных интерфейса: первый определяет ключи, а вторые – значения. Из-за того, что ключи в типе данных Map не могут повторяться по названию, крайне важно, чтобы в определении ключей были выбраны неповторяющиеся значения. Например, для кошек необходимо завести ещё одно свойство – номер международного ветеринарного паспорта, которое гарантированно не будет повторяться среди разных объектов.

Рассмотрим группировку объектов по критериям.

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

Другой способ группировки – по условию. Иными словами, если эле-мент удовлетворяет некоторому условию, переданному в аргументе метода группировки, то он добавляется в результирующий набор Map.

Дополнительные методы Collectors.counting и Collectors.summing используются для подсчёта количества элементов в каждой из групп и подсчёта общей суммы значений соответственно.

Метод Collectors.joining позволяет конкатенировать (складывать) строчные элементы, разделяя их делиметром (delimeter) или разделителем.

Методами maxBy и minBy можно для каждой группы вынести один максимальный или минимальный по какому-то критерию элемент.


Следующие методы: anyMatch, allMatch и noneMatch выполняют проверку на соответствие элементов потока данных некоторому заданному пре-дикату. Результатом выполнения всегда будет булево значение.

.anyMatch – проверяет удовлетворяет хотя бы один элемент из набора заданному условию.

.allMatch – удовлетворяют ли все элементы заданному условию.

.noneMatch – ни один элемент не удовлетворяет заданному условию.


Методы findFirst и findAny возвращают первый подходящий под условие или же произвольный элемент соответственно. В обоих случаях подходящих под условие объектов может и не быть, поэтому оба метода возвращают контейнер Optional.