Java Stream API


Содержание

Stream API в Java

В Java 8 появилось довольно важное нововведение под названием Stream. И здесь имеются в виду не потоки ввода/вывода. Stream — это абстракция, позволяющая с любыми объектами работать как с потоками данных. Порой это чем-то похоже на выполнение запросов к БД. Рассмотрим несколько типовых задач, с которыми часто сталкивается каждый разработчик.

Объединение нескольких строк в одну

Наверняка вам приходилось генерить одну строку из нескольких других, разделённых запятыми. При этом после последнего элемента запятой быть не должно. Знакомо? В java 8 это делается так:

Мы создаём новый поток из простых строк, а затем собираем их в одну при помощи метода collect(). В результате получим следующую строку:

Сгенерить N одинаковых элементов

Сгенерим три знака вопроса и вставим между ними запятые, аналогично предыдущей задаче:

Работа с произвольными типами

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

public class Person <

private final String name;
private final int age;

public Person(String name, int age) <
this .name = name;
this .age = age;
>

public String getName() <
return name;
>

public int getAge() <
return age;
>

@Override
public String toString() <
return String.format( «%s (%s)» , name, age);
>
>

Теперь инициализируем коллекцию, которую потом будем преобразовывать.

persons = new ArrayList<>();
persons.add( new Person( «Боб» , 25 ));
persons.add( new Person( «Алиса» , 20 ));
persons.add( new Person( «Николай Петрович» , 60 ));

Фильтрация элементов

Выберем всех пользователей не старше 30 лет и выведем каждого из них на экран:

Обратите внимание, что Stream можно получить из любой коллекции при помощи метода stream(). Затем в методе filter() используем лямбда-выражение (вместо p можно использовать любое другое имя). В качестве выражения фильтрации можно использовать абсолютно любое выражение, возвращающее тип boolean. И наконец, для каждого из оставшихся элементов мы вызываем статическую функцию println(). Обратите внимание на её особую запись. Эту запись можно использовать в стримах для любого метода, принимающего один параметр.

В результате на экране увидим Боба и Алису, а вот Николаю Петровичу не повезло:

Боб (25)
Алиса (20)

Среднее значение

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

Для метода Person.getAge() опять используем сокращённую запись. В результате получим, что средний возраст составляет 35,0 лет.

Преобразование элементов

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

Чтение файла в строку

Прочитать все строки текстового файла и объединить их в одну строку можно так:

Обратите внимание, что для объединения строк здесь используется специальный метод System.lineSeparator(), возвращающий один или два символа перевода строки в зависимости от вашей ОС.

Запись коллекции строк в файл

Похожим образом производится и запись строк в файл:

На выходе получим файл, состоящий из двух строк.

Java Stream API

В ходе вебинара мы на практических примерах изучим Java Stream API —

инструмент работы с коллекциями. Его задача — упростить работу с наборами данных: фильтрацию, сортировку и другие манипуляции с данными.

Что вы узнаете?

  • Познакомитесь с базовыми понятиями Stream API.
  • Научитесь принципам составления последовательности обработки коллекций.
  • Научитесь экономить время и нервы при работе с данными.
  • Увидите применение Stream API на практике.

Для кого это занятие?

  • Для тех, кто имеет хотя бы минимальный опыт работы с коллекциями Java.
  • Для опытных программистов, которые переходят на Java 8 и знакомятся с её особенностями.
  • Для тех, кто хочет больше знать о принципах обработки данных в Java.

Java Stream API

Lambda expressions are introduced in Java 8 and are touted to be the biggest feature of Java 8 — a compact way of passing around behavior. Lambda expression facilitates functional programming, and simplifies the development a lot.

Lambdas allows to specify a block of code which should be executed later. If a method expects a functional interface as parameter it is possible to pass in the lambda expression instead.

A lambda expression is characterized by the following syntax

Following are the important characteristics of a lambda expression

  • It is an anonymous function.
  • Optional type declaration. No need to declare the type of a parameter. The compiler can inference the same from the value of the parameter.
  • Optional parenthesis around parameter. No need to declare a single parameter in parenthesis. For multiple parameters, parentheses are required.
  • Optional curly braces. No need to use curly braces in expression body if the body contains a single statement.
  • Optional return keyword. The compiler automatically returns the value if the body has a single expression to return the value. Curly braces are required to indicate that expression returns a value.
  • Lambda expressions works nicely together only with functional interfaces. You cannot use lambda expressions with an interface with more than one abstract method.

An interface with exactly one abstract method is called Functional Interface. @FunctionalInterface annotation is added so that we can mark an interface as functional interface.

The Java compiler automatically identifies functional interfaces. The only requirement is that they have only one abstract method. However, is possible to capture the design intent with a @FunctionalInterface annotation.

Several default Java interfaces are functional interfaces:

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.io.FileFilter
  • java.util.Comparator
  • java.beans.PropertyChangeListener

Following are the important points to be considered in the above example.

  • Lambda expressions are used primarily to define inline implementation of a functional interface, i.e., an interface with a single method only.
  • Lambda expression eliminates the need of anonymous class and gives a very simple yet powerful functional programming capability to Java.

We can use new feature of Java 8 method reference for referencing to static methods, instance methods, constructors using new operator. Method references help to point to methods by their names. A method reference is described using :: (double colon) symbol.

Let’s look into an example of method referencing to get a more clear picture.

Difference between a lambda expression and a closure

The Java programming language supports lambdas but not closures. A lambda is an anonymous function, e.g., it can be defined as parameter. Closures are code fragments or code blocks which can be used without being a method or a class. This means that a closure can access variables not defined in its parameter list and that it can also be assigned to a variable.

Let’s look at some use case examples of java lambda expression.

  • A boolean expression: (List list) -> list.isEmpty()
  • Creating objects: () -> new Movie()
  • Consuming from an object: (Movie m) ->
  • Select/extract from an object: (String s) -> s.length()
  • Produce a single value by performing computation on two values: (int a, int b) -> a * b
  • Compare two objects: (Movie m1, Movie m2) -> m1.getYear().compareTo(m2.getMovie())

Stream API is a new abstract layer introduced in Java 8. Using stream, you can process data in a declarative way similar to SQL statements. Stream API represents a sequence of elements from a source, which supports different kind of operations to perform computations upon those elements. Following are the characteristics of a Stream.

  • Sequence of elements. A stream provides a set of elements of specific type in a sequential manner. A stream gets/computes elements on demand. It never stores the elements.
  • Source. Stream takes Collections , Arrays , or I/O resources as input source.
  • Aggregate operations. Stream supports aggregate operations like filter , map , limit , reduce , find , match , and so on.
  • Pipelining. Most of the stream operations return stream itself so that their result can be pipelined. These operations are called intermediate operations and their function is to take input, process them, and return output to the target.
  • Automatic iterations. Stream operations do the iterations internally over the source elements provided, in contrast to Collections where explicit iteration is required.

Collections vs Streams. Collections are in-memory data structures which hold elements within it. Each element in the collection is computed before it actually becomes a part of that collection. On the other hand Streams are fixed data structures which computes the elements on-demand basis. The Java 8 Streams can be seen as lazily constructed Collections, where the values are computed when user demands for it.

Stream operations are either intermediate or terminal. Intermediate operations return a stream so we can chain multiple intermediate operations without using semicolons. Terminal operations are either void or return a non-stream result. Methods filter , map and sorted are intermediate operations whereas forEach is a terminal operation.

Most stream operations accept some kind of lambda expression parameter, a functional interface specifying the exact behavior of the operation.

With Java 8, Collection interface has two methods to generate a Stream.

  • Method stream() returns a sequential stream considering collection as its source.
  • Method parallelStream() returns a parallel Stream considering collection as its source.

The limit() method is used to reduce the size of the stream. The following code segment shows how to print 10 random numbers using limit() .

The map() method is used to map each element to its corresponding result. In other words, for each item in the collection you create a new object based on that item. The following code segment prints unique squares of numbers using map() .

The filter() method is used to eliminate elements based on a criteria. The following code segment prints a count of empty strings using filter() .

The method forEach() is used to iterate each element of the stream. The following code segment shows how to print 10 random numbers using forEach() .

The sorted() method is used to sort the stream. The following code segment shows how to print 10 random numbers in a sorted order.

Collectors are used to combine the result of processing on the elements of a stream. Collectors can be used to return a list or a string.

Statistics collectors are introduced to calculate all statistics when stream processing is being done.

Various matching operations ( allMatch , anyMatch , noneMatch ) can be used to check whether a certain predicate matches the stream. All of those operations are terminal and return a boolean result.

The reduce() method performs a reduction on the elements of the stream with the given function. The result is an Optional holding the reduced value.

The Optional class acts as a wrapper around a value that may or may not be null, and is used to reduce the frequency of NullPointerException in applications that take advantage of it.

The findFirst() method will return first element from stream and then will not process any more element.

Besides regular object streams Java 8 ships with special kinds of streams for working with the primitive data types int , long and double . As you might have guessed it’s IntStream , LongStream and DoubleStream .

IntStreams can replace the regular for-loop utilizing IntStream.range()

The range(int startInclusive, int endExclusive) method creates an ordered stream from first parameter to the second parameter. It increments the value of subsequent elements with the step equal to 1. The result doesn’t include the last parameter, it is just an upper bound of the sequence.

The rangeClosed(int startInclusive, int endInclusive) method does the same with only one difference – the second element is included. These two methods can be used to generate any of the three types of streams of primitives.

EasyJava

Java в примерах для начинающих

Java 8 Stream API, часть вторая: map/reduce


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

Map/Reduce

Map/Reduce это вообщем-то модный buzzword. На самом деле Map/Reduce это очень простой шаблон проектирования, описывающий работу с наборами данных в два шага: на первом шаге выполняются (параллельные) операции над набором, на втором шаге результаты первого шага объединяются.

Интерфейс Stream определяет методы mapTo * ( ) , которые возвращают особые реализации Stream, имеющие методы average ( ) , sum ( ) , min ( ) , max ( ) , которые выполняют соответствующие арифметические действия над элементами набора данных.

В случае, когда нужно обработать поток, к которому операции mapTo * ( ) неприменимы, например из-за типа данных, можно использовать функцию reduce, которая принимает два параметра: начальное значение, оно же значение по умолчанию, и лямбда-выражение с двумя аргументами: первый хранит результат предыдущего вычисления, второй — текущее значение. Пример — вычисление списка уникальных символов в логинах пользователей:

Java Stream API

We recommend upgrading to the latest Google Chrome or Firefox.

Permalink

Type Name Latest commit message Commit time
..
Failed to load latest commit information.
BuildTests.java [JPO-14] Add new modules and do refactoring in old modules Jan 30, 2020
CollectAndToArrayTests.java [JPO-14] Add new modules and do refactoring in old modules Jan 30, 2020
DistinctTests.java [JPO-14] Add new modules and do refactoring in old modules Jan 30, 2020
FindAndMatchTests.java [JPO-14] Add new modules and do refactoring in old modules Jan 30, 2020
FiterAndCountTests.java [JPO-14] Add new modules and do refactoring in old modules Jan 30, 2020
ForEachAndPeekTests.java [JPO-14] Add new modules and do refactoring in old modules Jan 30, 2020
LimitAndSkipTests.java [JPO-14] Add new modules and do refactoring in old modules Jan 30, 2020
MapTests.java [JPO-14] Add new modules and do refactoring in old modules Jan 30, 2020
MaxMinTests.java [JPO-14] Add new modules and do refactoring in old modules Jan 30, 2020
ReduceTests.java [JPO-14] Add new modules and do refactoring in old modules Jan 30, 2020
SortedTests.java [JPO-14] Add new modules and do refactoring in old modules Jan 30, 2020
readme.md Update readme.md Apr 23, 2020

Несмотря на то, что Java 8 вышла уже достаточно давно, далеко не все программисты используют её новые возможности, кого-то останавливает то, что рабочие проекты слишком сложно перевести с Java 7 или даже Java 6, кого-то использование в своих проектах GWT, кто-то делает проекты под Android и не хочет или не может использовать сторонние библиотеки для реализации лямбд и Stream Api. Однако знание лямбд и Stream Api для программиста Java зачастую требуют на собеседованиях, ну и просто будет полезно при переходе на проект где используется Java 8. Я хотел бы предложить вам краткую шпаргалку по Stream Api с практическими примерами реализации различных задач с новым функциональным подходом. Знания лямбд и функционального программирования не потребуется (я постарался дать примеры так, чтобы все было понятно), уровень от самого базового знания Java и выше. Также, так как это шпаргалка, статья может использоваться, чтобы быстро вспомнить как работает та или иная особенность Java Stream Api. Краткое перечисление возможностей основных функций дано в начале статьи. Для тех кто совсем не знает что такое Stream Api Stream API это новый способ работать со структурами данных в функциональном стиле. Чаще всего с помощью stream в Java 8 работают с коллекциями, но на самом деле этот механизм может использоваться для самых различных данных. Stream Api позволяет писать обработку структур данных в стиле SQL, то если раньше задача получить сумму всех нечетных чисел из коллекции решалась следующим кодом:

То с помощью Stream Api можно решить такую задачу в функциональном стиле:

Более того, Stream Api позволяет решать задачу параллельно лишь изменив stream() на parallelStream() без всякого лишнего кода, т.е.

Уже делает код параллельным, без всяких семафоров, синхронизаций, рисков взаимных блокировок и т.п. Давайте начнем с начала, а именно с создания объектов stream в Java 8.

I. Способы создания стримов

Перечислим несколько способов создать стрим

Способ создания стрима Шаблон создания Пример
  1. Классический: Создание стрима из коллекции | collection.stream() | Collection collection = Arrays.asList(«a1», «a2», «a3»); Stream streamFromCollection = collection.stream(); |
  2. Создание стрима из значений | Stream.of(значение1, . значениеN) Stream streamFromValues = Stream.of(«a1», «a2», «a3»); |
  3. Создание стрима из массива | Arrays.stream(массив) | String[] array = <"a1","a2","a3">; Stream streamFromArrays = Arrays.stream(array); |
  4. Создание стрима из файла (каждая строка в файле будет отдельным элементом в стриме) | Files.lines(путь_к_файлу) | Stream streamFromFiles = Files.lines(Paths.get(«file.txt»)) |
  5. Создание стрима из строки | «строка».chars() | IntStream streamFromString = «123».chars() |
  6. С помощью Stream.builder | Stream.builder().add(. ). build() | Stream.builder().add(«a1»).add(«a2»).add(«a3»).build() |
  7. Создание параллельного стрима | collection.parallelStream() | Stream stream = collection.parallelStream(); |
  8. Создание бесконечных стрима с помощью Stream.iterate | Stream.iterate(начальное_условие, выражение_генерации) | Stream streamFromIterate = Stream.iterate(1, n -> n + 1) |
  9. Создание бесконечных стрима с помощью Stream.generate | Stream.generate(выражение_генерации) | Stream streamFromGenerate = Stream.generate(() -> «a1») |

В принципе, кроме последних двух способов создания стрима, все не отличается от обычных способов создания коллекций. Последние два способа служат для генерации бесконечных стримов, в iterate задается начальное условие и выражение получение следующего значения из предыдущего, то есть Stream.iterate(1, n -> n + 1) будет выдавать значения 1, 2, 3, 4, . N. Stream.generate служит для генерации константных и случайных значений, он просто выдает значения соответствующие выражению, в данном примере, он будет выдавать бесконечное количество значений «a1». Для тех кто не знает лямбды Выражение n -> n + 1, это просто аналог выражения Integer func(Integer n) < return n+1;>, а выражение () -> «a1» аналог выражения String func() < return "a1";>обернутых в анонимный класс.

Так же этот пример можно найти на github’e

II. Методы работы со стримами

Java Stream API предлагает два вида методов:

Конвейерные — возвращают другой stream, то есть работают как builder,

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

О том чем отличаются конвейерные и терминальные методы

Общее правило: у stream’a может быть сколько угодно вызовов конвейерных вызовов и в конце один терминальный, при этом все конвейерные методы выполняются лениво и пока не будет вызван терминальный метод никаких действий на самом деле не происходит, так же как создать объект Thread или Runnable, но не вызвать у него start. В целом, этот механизм похож на конструирования SQL запросов, может быть сколько угодно вложенных Select’ов и только один результат в итоге. Например, в выражении collection.stream().filter((s) -> s.contains(«1»)).skip(2).findFirst() , filter и skip — конвейерные, а findFirst — терминальный, он возвращает объект Optional и это заканчивает работу со stream’ом .

2.1 Краткое описание конвейерных методов работы со стримами

Метод stream Описание Пример
filter Отфильтровывает записи, возвращает только записи, соответствующие условию collection.stream().filter(«a1»::equals).count()
skip Позволяет пропустить N первых элементов collection.stream().skip(collection.size() — 1).findFirst().orElse(«1»)
distinct Возвращает стрим без дубликатов (для метода equals) collection.stream().distinct().collect(Collectors.toList())
map Преобразует каждый элемент стрима collection.stream().map((s) -> s + «_1»).collect(Collectors.toList())
peek Возвращает тот же стрим, но применяет функцию к каждому элементу стрима collection.stream().map(String::toUpperCase).peek((e) -> System.out.print(«,» + e)). collect(Collectors.toList())
limit Позволяет ограничить выборку определенным количеством первых элементов collection.stream().limit(2).collect(Collectors.toList())
sorted Позволяет сортировать значения либо в натуральном порядке, либо задавая Comparator collection.stream().sorted().collect(Collectors.toList())
mapToInt, mapToDouble, mapToLong Аналог map, но возвращает числовой стрим (то есть стрим из числовых примитивов) collection.stream().mapToInt((s) -> Integer.parseInt(s)).toArray()
flatMap, flatMapToInt, flatMapToDouble, flatMapToLong Похоже на map, но может создавать из одного элемента несколько collection.stream().flatMap((p) -> Arrays.asList(p.split(«,»)).stream()).toArray(String[]::new)

2.2 Краткое описание терминальных методов работы со стримами

Метод stream Описание Пример
findFirst Возвращает первый элемент из стрима (возвращает Optional) collection.stream().findFirst().orElse(«1»)
findAny Возвращает любой подходящий элемент из стрима (возвращает Optional) collection.stream().findAny().orElse(«1»)
collect Представление результатов в виде коллекций и других структур данных collection.stream().filter((s) -> s.contains(«1»)).collect(Collectors.toList())
count Возвращает количество элементов в стриме collection.stream().filter(«a1»::equals).count()
anyMatch Возвращает true, если условие выполняется хотя бы для одного элемента collection.stream().anyMatch(«a1»::equals)
noneMatch Возвращает true, если условие не выполняется ни для одного элемента collection.stream().noneMatch(«a8»::equals)
allMatch Возвращает true, если условие выполняется для всех элементов collection.stream().allMatch((s) -> s.contains(«1»))
min Возвращает минимальный элемент, в качестве условия использует компаратор collection.stream().min(String::compareTo).get()
max Возвращает максимальный элемент, в качестве условия использует компаратор collection.stream().max(String::compareTo).get()
forEach Применяет функцию к каждому объекту стрима, порядок при параллельном выполнении не гарантируется set.stream().forEach((p) -> p.append(«_1»));
forEachOrdered Применяет функцию к каждому объекту стрима, сохранение порядка элементов гарантирует list.stream().forEachOrdered((p) -> p.append(«_new»));
toArray Возвращает массив значений стрима collection.stream().map(String::toUpperCase).toArray(String[]::new);
reduce Позволяет выполнять агрегатные функции на всей коллекцией и возвращать один результат collection.stream().reduce((s1, s2) -> s1 + s2).orElse(0)

Обратите внимание методы findFirst, findAny, anyMatch это short-circuiting методы, то есть обход стримов организуется таким образом чтобы найти подходящий элемент максимально быстро, а не обходить весь изначальный стрим.

2.3 Краткое описание дополнительных методов у числовых стримов

Метод stream Описание Пример
sum Возвращает сумму всех чисел collection.stream().mapToInt((s) -> Integer.parseInt(s)).sum()
average Возвращает среднее арифметическое всех чисел collection.stream().mapToInt((s) -> Integer.parseInt(s)).average()
mapToObj Преобразует числовой стрим обратно в объектный intStream.mapToObj((id) -> new Key(id)).toArray()

2.4 Несколько других полезных методов стримов

Метод stream Описание
isParallel Узнать является ли стрим параллельным
parallel Вернуть параллельный стрим, если стрим уже параллельный, то может вернуть самого себя
sequential Вернуть последовательный стрим, если стрим уже последовательный, то может вернуть самого себя

С помощью, методов parallel и sequential можно определять какие операции могут быть параллельными, а какие только последовательными. Так же из любого последовательного стрима можно сделать параллельный и наоборот, то есть:

Внимание: крайне не рекомендуется использовать параллельные стримы для сколько-нибудь долгих операций (получение данных из базы, сетевых соединений), так как все параллельные стримы работают c одним пулом fork/join и такие долгие операции могут остановить работу всех параллельных стримов в JVM из-за того отсутствия доступных потоков в пуле, т.е. параллельные стримы стоит использовать лишь для коротких операций, где счет идет на миллисекунды, но не для тех где счет может идти на секунды и минуты.

III. Примеры работы с методами стримов

Рассмотрим работу с методами на различных задачах, обычно требующихся при работе с коллекциями.

3.1 Примеры использования filter, findFirst, findAny, skip, limit и count

Условие: дана коллекция строк Arrays.asList(«a1», «a2», «a3», «a1») , давайте посмотрим как её можно обрабатывать используя методы filter, findFirst, findAny, skip и count :

Задача Код примера Результат
Вернуть количество вхождений объекта «a1» collection.stream().filter(«a1»::equals).count() 2
Вернуть первый элемент коллекции или 0, если коллекция пуста collection.stream().findFirst().orElse(0) a1
Вернуть последний элемент коллекции или «empty», если коллекция пуста collection.stream().skip(collection.size() — 1).findAny().orElse(«empty») a1
Найти элемент в коллекции равный «a3» или кинуть ошибку collection.stream().filter(«a3»::equals).findFirst().get() a3
Вернуть третий элемент коллекции по порядку collection.stream().skip(2).findFirst().get() a3
Вернуть два элемента начиная со второго collection.stream().skip(1).limit(2).toArray() [a2, a3]
Выбрать все элементы по шаблону collection.stream().filter((s) -> s.contains(«1»)).collect(Collectors.toList()) [a1, a1]

Обратите внимание, что методы findFirst и findAny возвращают новый тип Optional , появившийся в Java 8 , для того чтобы избежать NullPointerException . Метод filter удобно использовать для выборки лишь определенного множества значений, а метод skip позволяет пропускать определенное количество элементов.

Если вы не знаете лямбды: Выражение «a3»::equals это аналог boolean func(s) < return "a3".equals(s);>, а (s) -> s.contains(«1») это аналог boolean func(s) < return s.contains("1");>обернутых в анонимный класс.

Условие: дана коллекция класс People (с полями name — имя, age — возраст, sex — пол), вида Arrays.asList( new People(«Вася», 16, Sex.MAN), new People(«Петя», 23, Sex.MAN), new People(«Елена», 42, Sex.WOMEN), new People(«Иван Иванович», 69, Sex.MAN)) . Давайте посмотрим примеры как работать с таким классом:

Задача Код примера Результат
Выбрать мужчин-военнообязанных (от 18 до 27 лет) peoples.stream().filter((p)-> p.getAge() >= 18 && p.getAge() p.getSex() == Sex.MAN). mapToInt(People::getAge).average().getAsDouble() 36.0
Найти кол-во потенциально работоспособных людей в выборке (т.е. от 18 лет и учитывая что женщины выходят в 55 лет, а мужчина в 60) peoples.stream().filter((p) -> p.getAge() >= 18).filter( (p) -> (p.getSex() == Sex.WOMEN && p.getAge()

3.2 Примеры использования distinct

Метод distinct возвращает stream без дубликатов, при этом для упорядоченного стрима (например, коллекция на основе list) порядок стабилен , для неупорядоченного — порядок не гарантируется. Рассмотрим результаты работы над коллекцией Collection ordered = Arrays.asList(«a1», «a2», «a2», «a3», «a1», «a2», «a2») и Collection nonOrdered = new HashSet<>(ordered) .

Задача Код примера Результат
Получение коллекции без дубликатов из неупорядоченного стрима nonOrdered.stream().distinct().collect(Collectors.toList()) [a1, a2, a3] — порядок не гарантируется
Получение коллекции без дубликатов из упорядоченного стрима ordered.stream().distinct().collect(Collectors.toList()); [a1, a2, a3] — порядок гарантируется

Обратите внимание:

  1. Если вы используете distinct с классом, у которого переопределен equals, обязательно так же корректно переопределить hashCode в соответствие с контрактом equals/hashCode (самое главное чтобы hashCode для всех equals объектов, возвращал одинаковое значение), иначе distinct может не удалить дубликаты (аналогично, как при использовании HashSet/HashMap),
  2. Если вы используете параллельные стримы и вам не важен порядок элементов после удаления дубликатов — намного лучше для производительности сделать сначала стрим неупорядоченным с помощь unordered(), а уже потом применять distinct(), так как подержание стабильности сортировки при параллельном стриме довольно затратно по ресурсам и distinct() на упорядоченным стриме будет выполнятся значительно дольше чем при неупорядоченном,

Так же этот пример можно найти на github’e

3.3 Примеры использования Match функций (anyMatch, allMatch, noneMatch)

Условие: дана коллекция строк Arrays.asList(«a1», «a2», «a3», «a1») , давайте посмотрим, как её можно обрабатывать используя Match функции

Задача Код примера Результат
Найти существуют ли хоть один «a1» элемент в коллекции collection.stream().anyMatch(«a1»::equals) true
Найти существуют ли хоть один «a8» элемент в коллекции collection.stream().anyMatch(«a8»::equals) false
Найти есть ли символ «1» у всех элементов коллекции collection.stream().allMatch((s) -> s.contains(«1»)) false
Проверить что не существуют ни одного «a7» элемента в коллекции collection.stream().noneMatch(«a7»::equals) true

Также этот пример можно найти на github’e

3.4 Примеры использования Map функций (map, mapToInt, FlatMap, FlatMapToInt)

Условие: даны две коллекции collection1 = Arrays.asList(«a1», «a2», «a3», «a1») и collection2 = Arrays.asList(«1,2,0», «4,5») , давайте посмотрим как её можно обрабатывать используя различные map функции

Задача Код примера Результат
Добавить «_1» к каждому элементу первой коллекции collection1.stream().map((s) -> s + «_1»).collect(Collectors.toList()) [a1_1, a2_1, a3_1, a1_1]
В первой коллекции убрать первый символ и вернуть массив чисел (int[]) collection1.stream().mapToInt((s) -> Integer.parseInt(s.substring(1))).toArray() [1, 2, 3, 1]
Из второй коллекции получить все числа, перечисленные через запятую из всех элементов collection2.stream().flatMap((p) -> Arrays.asList(p.split(«,»)).stream()).toArray(String[]::new) [1, 2, 0, 4, 5]
Из второй коллекции получить сумму всех чисел, перечисленных через запятую collection2.stream().flatMapToInt((p) -> Arrays.asList(p.split(«,»)).stream().mapToInt(Integer::parseInt)).sum() 12

Обратите внимание: все map функции могут вернуть объект другого типа (класса), то есть map может работать со стримом строк, а на выходе дать Stream из значений Integer или получать класс людей People, а возвращать класс Office, где эти люди работают и т.п., flatMap (flatMapToInt и т.п.) на выходе должны возвращать стрим с одним, несколькими или ни одним элементов для каждого элемента входящего стрима (см. последние два примера). Так же этот пример можно найти на github’e

3.5 Примеры использования Sorted функции

Условие: даны две коллекции коллекция строк Arrays.asList(«a1», «a4», «a3», «a2», «a1», «a4») и коллекция людей класса People (с полями name — имя, age — возраст, sex — пол), вида Arrays.asList( new People(«Вася», 16, Sex.MAN), new People(«Петя», 23, Sex.MAN), new People(«Елена», 42, Sex.WOMEN), new People(«Иван Иванович», 69, Sex.MAN)). Давайте посмотрим примеры как их можно сортировать:

Задача Код примера Результат
Отсортировать коллекцию строк по алфавиту collection.stream().sorted().collect(Collectors.toList()) [a1, a1, a2, a3, a4, a4]
Отсортировать коллекцию строк по алфавиту в обратном порядке collection.stream().sorted((o1, o2) -> -o1.compareTo(o2)).collect(Collectors.toList()) [a4, a4, a3, a2, a1, a1]
Отсортировать коллекцию строк по алфавиту и убрать дубликаты collection.stream().sorted().distinct().collect(Collectors.toList()) [a1, a2, a3, a4]
Отсортировать коллекцию строк по алфавиту в обратном порядке и убрать дубликаты collection.stream().sorted((o1, o2) -> -o1.compareTo(o2)).distinct().collect(Collectors.toList()) [a4, a3, a2, a1]
Отсортировать коллекцию людей по имени в обратном алфавитном порядке peoples.stream().sorted((o1,o2) -> -o1.getName().compareTo(o2.getName())).collect(Collectors.toList()) [<'Петя'>, <'Иван Иванович'>, <'Елена'>, <'Вася'>]
Отсортировать коллекцию людей сначала по полу, а потом по возрасту peoples.stream().sorted((o1, o2) -> o1.getSex() != o2.getSex() ? o1.getSex(). compareTo(o2.getSex()) : o1.getAge().compareTo(o2.getAge())).collect(Collectors.toList()) [<'Вася'>, <'Петя'>, <'Иван Иванович'>, <'Елена'>]

Так же этот пример можно найти на github’e

3.6 Примеры использования Max и Min функций

Условие: дана коллекция строк Arrays.asList(«a1», «a2», «a3», «a1»), и коллекция класса Peoples из прошлых примеров про Sorted и Filter функции.

Задача Код примера Результат
Найти максимальное значение среди коллекции строк collection.stream().max(String::compareTo).get() a3
Найти минимальное значение среди коллекции строк collection.stream().min(String::compareTo).get() a1
Найдем человека с максимальным возрастом peoples.stream().max((p1, p2) -> p1.getAge().compareTo(p2.getAge())).get()
Найдем человека с минимальным возрастом peoples.stream().min((p1, p2) -> p1.getAge().compareTo(p2.getAge())).get()

Так же этот пример можно найти на github’e

3.7 Примеры использования ForEach и Peek функций

Обе ForEach и Peek по сути делают одно и тоже, меняют свойства объектов в стриме, единственная разница между ними в том что ForEach терминальная и она заканчивает работу со стримом, в то время как Peek конвейерная и работа со стримом продолжается. Например, есть коллекция: Collection list = Arrays.asList(new StringBuilder(«a1»), new StringBuilder(«a2»), new StringBuilder(«a3»)); И нужно добавить к каждому элементу «_new», то для ForEach код будет list.stream().forEachOrdered((p) -> p.append(«_new»)); // list — содержит [a1_new, a2_new, a3_new] а для peek код будет List newList = list.stream().peek((p) -> p.append(«_new»)).collect(Collectors.toList()); // и list и newList содержат [a1_new, a2_new, a3_new] Так же этот пример можно найти на github’e

3.8 Примеры использования Reduce функции

Метод reduce позволяет выполнять агрегатные функции на всей коллекцией (такие как сумма, нахождение минимального или максимального значение и т.п.), он возвращает одно значение для стрима, функция получает два аргумента — значение полученное на прошлых шагах и текущее значение. Условие: Дана коллекция чисел Arrays.asList(1, 2, 3, 4, 2) выполним над ними несколько действий используя reduce.

Задача Код примера Результат
Получить сумму чисел или вернуть 0 collection.stream().reduce((s1, s2) -> s1 + s2).orElse(0) 12
Вернуть максимум или -1 collection.stream().reduce(Integer::max).orElse(-1) 4
Вернуть сумму нечетных чисел или 0 collection.stream().filter(o -> o % 2 != 0).reduce((s1, s2) -> s1 + s2).orElse(0) 4

Также этот пример можно найти на github’e

3.9 Примеры использования toArray и collect функции

Если с toArray все просто, можно либо вызвать toArray() получить Object[], либо toArray(T[]::new) — получив массив типа T, то collect позволяет много возможностей преобразовать значение в коллекцию, map’у или любой другой тип. Для этого используются статические методы из Collectors, например преобразование в List будет stream.collect(Collectors.toList()). Давайте рассмотрим статические методы из Collectors:

Метод Описание
toList, toCollection, toSet представляют стрим в виде списка, коллекции или множества
toConcurrentMap, toMap позволяют преобразовать стрим в map
averagingInt, averagingDouble, averagingLong возвращают среднее значение
summingInt, summingDouble, summingLong возвращает сумму
summarizingInt, summarizingDouble, summarizingLong возвращают SummaryStatistics с разными агрегатными значениями
partitioningBy разделяет коллекцию на две части по соответствию условию и возвращает их как Map
groupingBy разделяет коллекцию на несколько частей и возвращает Map
mapping дополнительные преобразования значений для сложных Collector’ов

Теперь давайте рассмотрим работу с collect и toArray на примерах: Условие: Дана коллекция чисел Arrays.asList(1, 2, 3, 4), рассмотрим работу collect и toArray с ней


Задача Код примера Результат
Получить сумму нечетных чисел numbers.stream().collect(Collectors.summingInt(((p) -> p % 2 == 1 ? p : 0))) 4
Вычесть от каждого элемента 1 и получить среднее numbers.stream().collect(Collectors.averagingInt((p) -> p — 1)) 1.5
Прибавить к числам 3 и получить статистику numbers.stream().collect(Collectors.summarizingInt((p) -> p + 3)) IntSummaryStatistics
Разделить числа на четные и нечетные numbers.stream().collect(Collectors.partitioningBy((p) -> p % 2 == 0))

Условие: Дана коллекция строк Arrays.asList(«a1», «b2», «c3», «a1»), рассмотрим работу collect и toArray с ней

Задача Код примера Результат
Получение списка без дубликатов strings.stream().distinct().collect(Collectors.toList()) [a1, b2, c3]
Получить массив строк без дубликатов и в верхнем регистре strings.stream().distinct().map(String::toUpperCase).toArray(String[]::new)
Объединить все элементы в одну строку через разделитель : и обернуть тегами . strings.stream().collect(Collectors.joining(» : «, » «, « «)) a1 : b2 : c3 : a1
Преобразовать в map, где первый символ ключ, второй символ значение strings.stream().distinct().collect(Collectors.toMap((p) -> p.substring(0, 1), (p) -> p.substring(1, 2)))
Преобразовать в map, сгруппировав по первому символу строки strings.stream().collect(Collectors.groupingBy((p) -> p.substring(0, 1)))
Преобразовать в map, сгруппировав по первому символу строки и объединим вторые символы через : strings.stream().collect(Collectors.groupingBy((p) -> p.substring(0, 1), Collectors.mapping((p) -> p.substring(1, 2), Collectors.joining(«:»))))

Также примеры можно найти на github’e

3.10 Пример создания собственного Collector’a

Кроме Collector’ов уже определенных в Collectors можно так же создать собственный Collector, Давайте рассмотрим пример как его можно создать. Метод определения пользовательского Collector’a:

Как видно из кода выше, для реализации своего Collector’a нужно определить три или четыре метода (метод_последней_обработки_аккумулятора не обязателен). Рассмотрим следующий кода, который мы писали до Java 8, чтобы объединить все строки коллекции:

И аналогичный код, который будет написан в Java 8

В общем-то, три метода легко понять из кода выше, их мы писали практически при каждой обработки коллекций, но вот что такое метод_соединения_двух_аккумуляторов? Это метод который нужен для параллельной обработки Collector’a, в данном случае при параллельном стриме коллекция может быть разделенной на две части (или больше частей), в каждой из которых будет свой аккумулятор StringBuilder и потом необходимо будет их объединить, то код до Java 8 при 2 потоках будет таким:

Напишем свой аналог

Так же примеры можно найти на github’e

IV. Заключение

Вот и все. Надеюсь, моя небольшая шпаргалка по работе со stream api была для вас полезной. Все исходники есть на github’е, удачи в написании хорошего кода.

The Java 8 Stream API Tutorial

Last modified: October 31, 2020

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

1. Overview

In this in-depth tutorial, we’ll go through the practical usage of Java 8 Streams from creation to parallel execution.

To understand this material, readers need to have a basic knowledge of Java 8 (lambda expressions, Optional, method references) and of the Stream API. If you aren’t familiar with these topics, please take a look at our previous articles – New Features in Java 8 and Introduction to Java 8 Streams.

Further reading:

Lambda Expressions and Functional Interfaces: Tips and Best Practices

Gu >The article discusses Java 8 Collectors, showing examples of built-in collectors, as well as showing how to build custom collector.

2. Stream Creation

There are many ways to create a stream instance of different sources. Once created, the instance will not modify its source, therefore allowing the creation of multiple instances from a single source.

2.1. Empty Stream

The empty() method should be used in case of a creation of an empty stream:

Its often the case that the empty() method is used upon creation to avoid returning null for streams with no element:

2.2. Stream of Collection

Stream can also be created of any type of Collection (Collection, List, Set):

2.3. Stream of Array

Array can also be a source of a Stream:

They can also be created out of an existing array or of a part of an array:

2.4. Stream.builder()

When builder is used the desired type should be additionally specified in the right part of the statement, otherwise the build() method will create an instance of the Stream :

2.5. Stream.generate()

The generate() method accepts a Supplier for element generation. As the resulting stream is infinite, developer should specify the desired size or the generate() method will work until it reaches the memory limit:

The code above creates a sequence of ten strings with the value – “element”.

2.6. Stream.iterate()

Another way of creating an infinite stream is by using the iterate() method:

The first element of the resulting stream is a first parameter of the iterate() method. For creating every following element the specified function is applied to the previous element. In the example above the second element will be 42.

2.7. Stream of Primitives

Java 8 offers a possibility to create streams out of three primitive types: int, long and double. As Stream is a generic interface and there is no way to use primitives as a type parameter with generics, three new special interfaces were created: IntStream, LongStream, DoubleStream.

Using the new interfaces alleviates unnecessary auto-boxing allows increased productivity:

The range(int startInclusive, int endExclusive) method creates an ordered stream from the first parameter to the second parameter. It increments the value of subsequent elements with the step equal to 1. The result doesn’t include the last parameter, it is just an upper bound of the sequence.

The rangeClosed(int startInclusive, int endInclusive) method does the same with only one difference – the second element is included. These two methods can be used to generate any of the three types of streams of primitives.

Since Java 8 the Random class provides a wide range of methods for generation streams of primitives. For example, the following code creates a DoubleStream, which has three elements:

2.8. Stream of String

String can also be used as a source for creating a stream.

With the help of the chars() method of the String class. Since there is no interface CharStream in JDK, the IntStream is used to represent a stream of chars instead.

The following example breaks a String into sub-strings according to specified RegEx:

2.9. Stream of File

Java NIO class Files allows to generate a Stream of a text file through the lines() method. Every line of the text becomes an element of the stream:

The Charset can be specified as an argument of the lines() method.

3. Referencing a Stream

It is possible to instantiate a stream and to have an accessible reference to it as long as only intermediate operations were called. Executing a terminal operation makes a stream inaccessible.

To demonstrate this we will forget for a while that the best practice is to chain sequence of operation. Besides its unnecessary verbosity, technically the following code is valid:

But an attempt to reuse the same reference after calling the terminal operation will trigger the IllegalStateException:

As the IllegalStateException is a RuntimeException, a compiler will not signalize about a problem. So, it is very important to remember that Java 8 streams can’t be reused.

This kind of behavior is logical because streams were designed to provide an ability to apply a finite sequence of operations to the source of elements in a functional style, but not to store elements.

So, to make previous code work properly some changes should be done:

4. Stream Pipeline

To perform a sequence of operations over the elements of the data source and aggregate their results, three parts are needed – the source, intermediate operation(s) and a terminal operation.

Intermediate operations return a new modified stream. For example, to create a new stream of the existing one without few elements the skip() method should be used:

If more than one modification is needed, intermediate operations can be chained. Assume that we also need to substitute every element of current Stream with a sub-string of first few chars. This will be done by chaining the skip() and the map() methods:

As you can see, the map() method takes a lambda expression as a parameter. If you want to learn more about lambdas take a look at our tutorial Lambda Expressions and Functional Interfaces: Tips and Best Practices.

A stream by itself is worthless, the real thing a user is interested in is a result of the terminal operation, which can be a value of some type or an action applied to every element of the stream. Only one terminal operation can be used per stream.

The right and most convenient way to use streams are by a stream pipeline, which is a chain of stream source, intermediate operations, and a terminal operation. For example:

5. Lazy Invocation

Intermediate operations are lazy. This means that they will be invoked only if it is necessary for the terminal operation execution.

To demonstrate this, imagine that we have method wasCalled(), which increments an inner counter every time it was called:

Let’s call method wasCalled() from operation filter():

As we have a source of three elements we can assume that method filter() will be called three times and the value of the counter variable will be 3. But running this code doesn’t change counter at all, it is still zero, so, the filter() method wasn’t called even once. The reason why – is missing of the terminal operation.

Let’s rewrite this code a little bit by adding a map() operation and a terminal operation – findFirst(). We will also add an ability to track an order of method calls with a help of logging:

Resulting log shows that the filter() method was called twice and the map() method just once. It is so because the pipeline executes vertically. In our example the first element of the stream didn’t satisfy filter’s predicate, then the filter() method was invoked for the second element, which passed the filter. Without calling the filter() for third element we went down through pipeline to the map() method.

The findFirst() operation satisfies by just one element. So, in this particular example the lazy invocation allowed to avoid two method calls – one for the filter() and one for the map().

6. Order of Execution

From the performance point of view, the right order is one of the most important aspects of chaining operations in the stream pipeline:

Execution of this code will increase the value of the counter by three. This means that the map() method of the stream was called three times. But the value of the size is one. So, resulting stream has just one element and we executed the expensive map() operations for no reason twice out of three times.

If we change the order of the skip() and the map() methods, the counter will increase only by one. So, the method map() will be called just once:

This brings us up to the rule: intermediate operations which reduce the size of the stream should be placed before operations which are applying to each element. So, keep such methods as skip(), filter(), distinct() at the top of your stream pipeline.

7. Stream Reduction

The API has many terminal operations which aggregate a stream to a type or to a primitive, for example, count(), max(), min(), sum(), but these operations work according to the predefined implementation. And what if a developer needs to customize a Stream’s reduction mechanism? There are two methods which allow to do this – the reduce() and the collect() methods.

7.1. The reduce() Method

There are three variations of this method, which differ by their signatures and returning types. They can have the following parameters:

identity – the initial value for an accumulator or a default value if a stream is empty and there is nothing to accumulate;

accumulator – a function which specifies a logic of aggregation of elements. As accumulator creates a new value for every step of reducing, the quantity of new values equals to the stream’s size and only the last value is useful. This is not very good for the performance.

combiner – a function which aggregates results of the accumulator. Combiner is called only in a parallel mode to reduce results of accumulators from different threads.

So, let’s look at these three methods in action:

reduced = 6 (1 + 2 + 3)

reducedTwoParams = 16 (10 + 1 + 2 + 3)

The result will be the same as in the previous example (16) and there will be no login which means, that combiner wasn’t called. To make a combiner work, a stream should be parallel:


The result here is different (36) and the combiner was called twice. Here the reduction works by the following algorithm: accumulator ran three times by adding every element of the stream to >

7.2. The collect() Method

Reduction of a stream can also be executed by another terminal operation – the collect() method. It accepts an argument of the type Collector, which specifies the mechanism of reduction. There are already created predefined collectors for most common operations. They can be accessed with the help of the Collectors type.

In this section we will use the following List as a source for all streams:

Converting a stream to the Collection (Collection, List or Set):

Reducing to String:

The joiner() method can have from one to three parameters (delimiter, prefix, suffix). The handiest thing about using joiner() – developer doesn’t need to check if the stream reaches its end to apply the suffix and not to apply a delimiter. Collector will take care of that.

Processing the average value of all numeric elements of the stream:

Processing the sum of all numeric elements of the stream:

Methods averagingXX(), summingXX() and summarizingXX() can work as with primitives (int, long, double) as with their wrapper classes (Integer, Long, Double). One more powerful feature of these methods is providing the mapping. So, developer doesn’t need to use an additional map() operation before the collect() method.

Collecting statistical information about stream’s elements:

By using the resulting instance of type IntSummaryStatistics developer can create a statistical report by applying toString() method. The result will be a String common to this one “IntSummaryStatistics”.

It is also easy to extract from this object separate values for count, sum, min, average by applying methods getCount(), getSum(), getMin(), getAverage(), getMax(). All these values can be extracted from a single pipeline.

Grouping of stream’s elements according to the specified function:

In the example above the stream was reduced to the Map which groups all products by their price.

Dividing stream’s elements into groups according to some predicate:

Pushing the collector to perform additional transformation:

In this particular case, the collector has converted a stream to a Set and then created the unmodifiable Set out of it.

Custom collector:

If for some reason, a custom collector should be created, the most easier and the less verbose way of doing so – is to use the method of() of the type Collector.

In this example, an instance of the Collector got reduced to the LinkedList

Parallel Streams

Before Java 8, parallelization was complex. Emerging of the ExecutorService and the ForkJoin simplified developer’s life a little bit, but they still should keep in mind how to create a specific executor, how to run it and so on. Java 8 introduced a way of accomplishing parallelism in a functional style.

The API allows creating parallel streams, which perform operations in a parallel mode. When the source of a stream is a Collection or an array it can be achieved with the help of the parallelStream() method:

If the source of stream is something different than a Collection or an array, the parallel() method should be used:

Under the hood, Stream API automatically uses the ForkJoin framework to execute operations in parallel. By default, the common thread pool will be used and there is no way (at least for now) to assign some custom thread pool to it. This can be overcome by using a custom set of parallel collectors.

When using streams in parallel mode, avoid blocking operations and use parallel mode when tasks need the similar amount of time to execute (if one task lasts much longer than the other, it can slow down the complete app’s workflow).

The stream in parallel mode can be converted back to the sequential mode by using the sequential() method:

Conclusions

The Stream API is a powerful but simple to understand set of tools for processing sequence of elements. It allows us to reduce a huge amount of boilerplate code, create more readable programs and improve app’s productivity when used properly.

In most of the code samples shown in this article streams were left unconsumed (we didn’t apply the close() method or a terminal operation). In a real app, don’t leave an instantiated streams unconsumed as that will lead to memory leaks.

The complete code samples that accompany the article are available over on GitHub.

Working with the Java Stream API

Java 8 Stream APIs are built keeping lambda expression in mind, and their power can be realized visibly when using them in Java code. The essence of the Stream APIs is in their ability to perform sophisticated operations in data manipulation such as searching, filtering, and mapping data. Conceptually, these operations resemble the peculiarities of database queries through SQL. In fact, in many cases they may be performed in parallel to attain the additional efficiency of data handling. The APIs described in the Java Stream API library are one of the advanced features of Java. A good grasp of the generics and lambda is a must to fully appreciate this feature. The article attempts to bring to you some of the qualities of this API and how to use them in a simple manner.

Overview of Stream

A stream basically represents a flow of data; it is a conduit through which elements are fed from a source, such as an array or a collection for computational needs. A stream does not provide a storage but a means to means streamline data. The operation performed on it produces an outcome without changing the source. For example, you can sort the stream results in the creation of a new stream that represents sorted data. The original data source remains unchanged, except that the outcome reflects the change in a new stream.

It is worth mentioning that the stream referring here is defined in java.util.stream. It is quite different from the IO stream supplied by the java.io package, though conceptually they may act in a similar fashion. The difference lies in their inherently built structure. To be specific, Java IO streams are abstractions that either produce or consume information. They are primarily linked to a physical device of the Java IO system. The idea is to make the streams behave in a symmetry irrespective of the type of devices involved in doing the IO operation. The stream APIs of java.util.stream, on the other hand, are functional in nature. They are more conducive in connection with lambda expressions. They are typically used to manipulate data associated with the data structure.

Operation Pipelining

In a lambda expression, the stream API code can be segmented into data source, operations performed on the data, and the terminal operation. The data source implies the source from where data comes in. For example, an object of List is a valid candidate of a data source. The operations performed in data refer to the action taken on the data, such as sorting or filtering. The terminal operation means what necessary actions are taken after the intermediate operation, such as persistence.

The following snippet provides an example of the previous three actions discussed.

If we want to create a new collection instance that would contain only the filtered items from the original collection, we may write the code in the following manner.

Observe that the ‘asteroid’ stream represents the data source upon which one or more intermediate operations applied. After we are done with intermediate operations, the outcome at the end is collected in a list object.

Here is another variation where we do not collect the items. Instead, we print the filtered items on screen.

Or, more simply, as:

With lambda expression, the Java compiler infers a lot of information, especially in relation to data types. The power comes from generics. The intriguing fact is that the filter method takes an instance of the Predicate interface. According to the Java API Documentation, Predicate is a functional interface that can be used as an assignment for lambda expression or method reference. It includes a few defined methods, and the one that is invoked in our case is the test method, which is passed as a single parameter and returns a boolean value. The intricacies involved in the concept are beyond the scope of this article. Refer to the Java API Documentation for Predicate, lambda expression, and generics to get further details.

The Stream Interfaces

The interface BaseStream is the foundation of all streams defined in the java.util.stream package and defines the core functionalities of its family. The header of this interface is defined as:

Here, T implies the type of elements in the stream and S implies the type of stream that extends BaseStream. The methods declared in this interface are as follows.

Descriptions void close() boolean isParallel() Iterator iterator Terminal Operation
Returns a new stream with a close handler. This handler is used or invoked when the stream is closed during intermediate operation. Returns a parallel stream. Return a sequential stream. Spliterator spliterator() Terminal Operation
Returns an unordered stream.
Descriptions long count() Terminal Operation
Filters the elements in the stream based upon the predicate supplied to it as the parameter. Stream map(Function mapFunc) Intermediate Operation
Finds and returns the maximum/minimum elements in the stream according to the ordering specified by he Comparator. Sorts the elements of the stream in natural order.

As you can see, the operations on a stream can be chained together (intermediate operations) and end with a terminal operation. Such a chain of stream operations is called stream pipeline.

2. Stream Pipeline

  • a source: can be a collection, an array or an I/O channel.
  • zero or more intermediate operations which produce new streams, such as filter , map , sorted , etc.
  • a terminal operation that produces a non-stream result such as a primitive value, a collection, or vo >forEach operation).

3. Intermediate Operations

One interesting point about intermediate operations is that they are lazily executed. That means they are not run until a terminal operation is executed.

The Stream API provides the following common intermediate operations:

  • map()
  • filter()
  • sorted()
  • limit()
  • distinct()

For a full list of intermediate operations, consult the Stream Javadoc.

4. Terminal Operations

Unlike lazily-executed terminate operations, a terminal operation is always eagerly executed. The common terminal operations provided by the Stream API include:

  • collect()
  • reduce()
  • forEach()

See the Stream Javadoc for a complete list of terminal operations supported.

5. Parallel Streams

When a stream executes in parallel, the Java runtime divides the stream into multiple sub streams. The aggregate operations iterate over and process these sub streams in parallel and then combine the results.

The advantage of parallel streams is performance increase on large amount of input elements, as the operations are executed currently by multiple threads on a multi-core CPU.

For example, the following code may execute stream operations in parallel:

The Collection’s stream() method returns a sequential stream. We can convert a sequential stream to a parallel stream by calling the parallel() method on the current stream. The following example shows a stream executes the sorted operation sequentially, and then execute the filter operation in parallel:

6. Streams and Lambda Expressions

Consider the following example:

Here, we use a Lambda expression in the filter operation and a static method reference in the forEach operation. The s parameter is of type Student because the stream is a sequence of Student objects.

NOTE: Some operations can transform a stream of type A to a stream of type B, such as the map operation in the following example:

In Lambda expressions used with the filter and map operations, the s parameter is of type Student . However the map operation produces a stream of Strings, so the name parameter in the Lambda expression in the forEach operation is of type String . So pay attention to this kind of transformation when using Lambda expressions.

7. Streams vs. Collections

On the other hand, a stream is not a data structure. A stream is a pipeline of operations that compute the elements on-demand. Though we can create a stream from a collection and apply a number of operations, the original collection doesn’t change. Hence streams cannot mutate data.

And a key characteristic of streams is that they can transform data, as operations on a stream can produce another data structure, like the map and collect operation as shown in the above examples.

References:

Other Java Collections Tutorials:

About the Author:

Nam Ha Minh is certified Java programmer (SCJP and SCWCD). He started programming with Java in the time of Java 1.4 and has been falling in love with Java since then. Make friend with him on Facebook.

Add comment

Comments

Please correct it to Good list from bad list
List listGoodStudents = new ArrayList();

Java Stream API

Lambda expressions are introduced in Java 8 and are touted to be the biggest feature of Java 8 — a compact way of passing around behavior. Lambda expression facilitates functional programming, and simplifies the development a lot.

Lambdas allows to specify a block of code which should be executed later. If a method expects a functional interface as parameter it is possible to pass in the lambda expression instead.

A lambda expression is characterized by the following syntax

Following are the important characteristics of a lambda expression

  • It is an anonymous function.
  • Optional type declaration. No need to declare the type of a parameter. The compiler can inference the same from the value of the parameter.
  • Optional parenthesis around parameter. No need to declare a single parameter in parenthesis. For multiple parameters, parentheses are required.
  • Optional curly braces. No need to use curly braces in expression body if the body contains a single statement.
  • Optional return keyword. The compiler automatically returns the value if the body has a single expression to return the value. Curly braces are required to indicate that expression returns a value.
  • Lambda expressions works nicely together only with functional interfaces. You cannot use lambda expressions with an interface with more than one abstract method.

An interface with exactly one abstract method is called Functional Interface. @FunctionalInterface annotation is added so that we can mark an interface as functional interface.

The Java compiler automatically identifies functional interfaces. The only requirement is that they have only one abstract method. However, is possible to capture the design intent with a @FunctionalInterface annotation.

Several default Java interfaces are functional interfaces:

  • java.lang.Runnable
  • java.util.concurrent.Callable
  • java.io.FileFilter
  • java.util.Comparator
  • java.beans.PropertyChangeListener

Following are the important points to be considered in the above example.

  • Lambda expressions are used primarily to define inline implementation of a functional interface, i.e., an interface with a single method only.
  • Lambda expression eliminates the need of anonymous class and gives a very simple yet powerful functional programming capability to Java.

We can use new feature of Java 8 method reference for referencing to static methods, instance methods, constructors using new operator. Method references help to point to methods by their names. A method reference is described using :: (double colon) symbol.

Let’s look into an example of method referencing to get a more clear picture.

Difference between a lambda expression and a closure

The Java programming language supports lambdas but not closures. A lambda is an anonymous function, e.g., it can be defined as parameter. Closures are code fragments or code blocks which can be used without being a method or a class. This means that a closure can access variables not defined in its parameter list and that it can also be assigned to a variable.

Let’s look at some use case examples of java lambda expression.

  • A boolean expression: (List list) -> list.isEmpty()
  • Creating objects: () -> new Movie()
  • Consuming from an object: (Movie m) ->
  • Select/extract from an object: (String s) -> s.length()
  • Produce a single value by performing computation on two values: (int a, int b) -> a * b
  • Compare two objects: (Movie m1, Movie m2) -> m1.getYear().compareTo(m2.getMovie())

Stream API is a new abstract layer introduced in Java 8. Using stream, you can process data in a declarative way similar to SQL statements. Stream API represents a sequence of elements from a source, which supports different kind of operations to perform computations upon those elements. Following are the characteristics of a Stream.

  • Sequence of elements. A stream provides a set of elements of specific type in a sequential manner. A stream gets/computes elements on demand. It never stores the elements.
  • Source. Stream takes Collections , Arrays , or I/O resources as input source.
  • Aggregate operations. Stream supports aggregate operations like filter , map , limit , reduce , find , match , and so on.
  • Pipelining. Most of the stream operations return stream itself so that their result can be pipelined. These operations are called intermediate operations and their function is to take input, process them, and return output to the target.
  • Automatic iterations. Stream operations do the iterations internally over the source elements provided, in contrast to Collections where explicit iteration is required.

Collections vs Streams. Collections are in-memory data structures which hold elements within it. Each element in the collection is computed before it actually becomes a part of that collection. On the other hand Streams are fixed data structures which computes the elements on-demand basis. The Java 8 Streams can be seen as lazily constructed Collections, where the values are computed when user demands for it.

Stream operations are either intermediate or terminal. Intermediate operations return a stream so we can chain multiple intermediate operations without using semicolons. Terminal operations are either void or return a non-stream result. Methods filter , map and sorted are intermediate operations whereas forEach is a terminal operation.

Most stream operations accept some kind of lambda expression parameter, a functional interface specifying the exact behavior of the operation.

With Java 8, Collection interface has two methods to generate a Stream.

  • Method stream() returns a sequential stream considering collection as its source.
  • Method parallelStream() returns a parallel Stream considering collection as its source.

The limit() method is used to reduce the size of the stream. The following code segment shows how to print 10 random numbers using limit() .

The map() method is used to map each element to its corresponding result. In other words, for each item in the collection you create a new object based on that item. The following code segment prints unique squares of numbers using map() .

The filter() method is used to eliminate elements based on a criteria. The following code segment prints a count of empty strings using filter() .

The method forEach() is used to iterate each element of the stream. The following code segment shows how to print 10 random numbers using forEach() .

The sorted() method is used to sort the stream. The following code segment shows how to print 10 random numbers in a sorted order.

Collectors are used to combine the result of processing on the elements of a stream. Collectors can be used to return a list or a string.

Statistics collectors are introduced to calculate all statistics when stream processing is being done.

Various matching operations ( allMatch , anyMatch , noneMatch ) can be used to check whether a certain predicate matches the stream. All of those operations are terminal and return a boolean result.

The reduce() method performs a reduction on the elements of the stream with the given function. The result is an Optional holding the reduced value.

The Optional class acts as a wrapper around a value that may or may not be null, and is used to reduce the frequency of NullPointerException in applications that take advantage of it.

The findFirst() method will return first element from stream and then will not process any more element.

Besides regular object streams Java 8 ships with special kinds of streams for working with the primitive data types int , long and double . As you might have guessed it’s IntStream , LongStream and DoubleStream .

IntStreams can replace the regular for-loop utilizing IntStream.range()

The range(int startInclusive, int endExclusive) method creates an ordered stream from first parameter to the second parameter. It increments the value of subsequent elements with the step equal to 1. The result doesn’t include the last parameter, it is just an upper bound of the sequence.

The rangeClosed(int startInclusive, int endInclusive) method does the same with only one difference – the second element is included. These two methods can be used to generate any of the three types of streams of primitives.

Java 8 Stream API, создание потоков и их основные особенности

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

Говоря поток, я имею ввиду Stream, т.е. поток данных (не поток выплнения Thread и не поток ввода вывода)
Говоря просто, поток похож на коллекцию тем что позволяет извлекать и обрабатывать данные, но потоку, в отличии от коллекции неважно как хранить данные.
Для потока самое важное — это те операции которые можно выполнить с данными. Фактически поток представляет собой конвейер операций

Давайте перечислим основные особенности потоков:

  1. Поток не хранит элементы, элементы храняться в источнике данных, например в коллекции
  2. В результате работы с данными потока, не меняются сами данные и их источник
  3. Поток неизменяемый, после обработки данных создаётся новый поток
  4. В потоке большинство оперций ленивые, т.е. выполняются по требованию
  5. Потоки содержат такой набор операций, которые легко можно распараллелить

Операции выполняемые с потоком, как мы выяснили, являются самым важным в потоках.
Операции деляться на:

промежуточные — могут быть отложены
завершающие — инициирует выполнение всех промежуточных операций

Об операциях мы поговорим в следующих статьях, а пока давайте поговорим о типах потоков

Типы потоков

Потоки представлены классом Stream, предназначен для хранения любых объектов, но при создании потока выплняется параметризация.
Так же есть специализированные типы потоков: IntStream, LongStream, DoubleStream.

Цукерберг рекомендует:  Навигационное меню с визуальным эффектом на CSS3
Понравилась статья? Поделиться с друзьями:
Все языки программирования для начинающих