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



Часто Stream API в Java8 используется для работы с коллекциями, позволяя писать код в функциональном стиле.
Удобство и простота методов способствуют интересу к данному функционалу у разработчиков с момента его выхода.
Итак, что такое Stream API в Java8? «Package java.util.stream» — «Classes to support functional-style operations on streams of elements, such as map-reduce transformations on collections». Попробую дать свой вариант перевода, фактически это — поддержка функционального стиля операций над потоками, такими как обработка и «свёртка» обработанных данных.

«Stream operations are divided into intermediate and terminal operations, and are combined to form stream pipelines. A stream pipeline consists of a source (such as a Collection, an array, a generator function, or an I/O channel); followed by zero or more intermediate operations such as Stream.filter or Stream.map; and a terminal operation such as Stream.forEach or Stream.reduce»описание с сайта.
Попробуем разобраться в этом определении. Авторы говорят нам о наличии промежуточных и конечных операций, которые объедены в форму конвейеров. Потоковые конвейеры содержат источник (например, коллекции и т.п.) за которым следуют промежуточные и конечные операции и приводятся их примеры. Тут стоит заметить, что все промежуточные операции над потоками — ленивые(LAZY). Они не будут исполнены, пока не будет вызвана терминальная (конечная) операция.

Еще одна интересная особенность, это – наличие parallelStream(). Данные возможности я использую для улучшения производительности при обработке больших объемов данных. Параллельные потоки позволят ускорить выполнение некоторых видов операций. Я использую данную возможность, когда знаю, что коллекция достаточно большая для обработки ее в «ForkJoin» варианте. Подробнее про ForkJoin читайте в предыдущей статье на эту тему — «Java 8 в параллель. Учимся создавать подзадачи и контролировать их выполнение».

Закончим с теоретической частью и перейдем к несложным примерам.
Пример показывает нахождение максимального и минимального значения из коллекции.
/**
 * Пример № 1
 * Нахождение максимального и минимального значений
 */
ArrayList<Integer> testValues = new ArrayList();
testValues.add(0,15);
testValues.add(1,1);
testValues.add(2,2);
testValues.add(3,100);
testValues.add(4,50);

Optional<Integer> maxValue = testValues.stream().max(Integer::compareTo);
System.out.println("MaxValue="+maxValue);
Optional<Integer> minValue = testValues.stream().min(Integer::compareTo);
System.out.println("MinValue="+minValue);

Немного усложним пример и добавим исключения (в виде null) при максимального значения в пример №2.
/**
 * Пример № 2
 * Нахождение максимального значения исключая null значения
 */
ArrayList<Integer> testValuesNull = new ArrayList();
testValuesNull.add(0,null);
testValuesNull.add(1,1);
testValuesNull.add(2,2);
testValuesNull.add(3,70);
testValuesNull.add(4,50);

Optional<Integer> maxValueNotNull =  testValuesNull.stream().filter((p) -> p != null).max(Integer::compareTo);
System.out.println("maxValueNotNull="+maxValueNotNull);

Усложним примеры. Создадим коллекцию «спортивный лагерь», состоящую из полей «Имя» и «Количество дней в спортивном лагере». Сам пример создания класса ниже.
public  class SportsCamp {
    private  String name; //Имя спортсмена
    private  Integer day; //Количество дней в спортивном лагере

    public SportsCamp(String name, int day) {

        this.name = name;
        this.day = day;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Integer getDay() {
        return day;
    }
    public void setDay(Integer day) {
        this.day = day;
    }
}

А теперь примеры работы с новыми данными:
import java.util.Arrays;
import java.util.Collection;

public class Start {
        public static void main(String[] args) {

            Collection<SportsCamp> sport = Arrays.asList(
                    new SportsCamp("Ivan", 5),
                    new SportsCamp("Petr", 7),
                    new SportsCamp("Ira", 10)
            );
            /**
             * Пример 3 
             * Поиск имени самого большого по продолжительности нахождения в лагере
             */
            String name = sport.stream().max((p1,p2) -> p1.getDay().compareTo(p2.getDay())).get().getName();
            System.out.println("Name="+name);
        }
}

В примере было найдено имя, Ирина, которая будет находиться в лагере всех дольше.
Преобразуем пример и создадим ситуацию, когда у нас вкралась ошибка, и одна из записей null в имени.
Collection<SportsCamp> sport = Arrays.asList(
        new SportsCamp("Ivan", 5),
        new SportsCamp( null, 15),
        new SportsCamp("Petr", 7),
        new SportsCamp("Ira", 10)
);

В этом случае вы получите результат, равный «Name=null».Согласитесь, что мы хотели не этого.Немного изменим поиск по коллекции на новый вариант.
/**
 * Пример № 4
 */
String nameTest = sport.stream().filter((p) -> p.getName() != null).max((p1, p2) -> p1.getDay().compareTo(p2.getDay())).get().getName();

Полученный результат, «Ira» — верен.
В примерах показано нам нахождение минимальных и максимальных значений по коллекциям с небольшими дополнениями в виде исключения null значений.
Как мы говорили доступные методы можно разделить на две большие группы промежуточные операции и конечные. Авторы могут называть их различно, например, вариант названия конвейерные и терминальные методы употребляется в литературе и статьях. При работе с методами существует одна конструктивная особенность, вы можете «накидывать» множество промежуточных операций, в конце производя вызов одного терминального метода.
В новом примере добавим сортировку и вывод определенного элемента, например, добавим фильтр по именам с встречающимся «Ivan» и произведем подсчет таких элементов (исключим null значения).
/**
 * Пример № 5
 */
long countName =  sport.stream().filter((p) -> p.getName() != null && p.getName().equals("Ivan")).count();
System.out.println("countName="+countName);

Добавив в коллекцию new SportsCamp(«Ivan», 17), получим результат равный «countName=2». Нашли две записи.
В данных примерах использовалось создание стрима из коллекции, доступны и другие варианты, например, создание стрима из требуемых значений, например, Stream streamFromValues = Stream.of(«test1», «test2», «test3»), возможны и другие варианты.
Как говорилось выше, у пользователей есть возможность использовать «обработку» используя parallelStream().
Немного изменив пример, получим новый вариант реализации:
long countNameParallel = sport.parallelStream().filter((p) -> p.getName() != null && p.getName().equals("Ivan")).count();
System.out.println("countNameParallel=" + countNameParallel);

Особенность этого варианта состоит в реализации параллельного стрима. Хочется обратить внимание, что parallelStream() оправданно использовать на мощных серверах(многоядерных) для больших коллекций. Я не даю четкого определения и точного размера коллекций, т.к. очень много параметров необходимо выявить и просчитать. Часто только тестирование может показать вам увеличение производительность.
Мы немного познакомились с простыми операциями, поняли отличие между конвейерными и терминальными операциями, попробовали и те и другие. А теперь давайте посмотрим примеры более сложных операций, например, collect и Map, Flat и Reduce.
Еще раз заглянем в официальную документацию документацию и попробуем реализовать свои примеры.
В новом примере попробуем преобразовать одну коллекцию в другую, по именам начинающимся с «I» и запишем это в List.
List<SportsCamp> onlyI = sport.stream().filter(p -> p.getName() != null &&  p.getName().startsWith("I")).collect(Collectors.toList());
System.out.println("SIZE="+onlyI.size());

Результат будет равен трём. Тут нужно обратить внимание, что порядок указания исключения null элементов значим.
Обратите внимание, что Collectors обладает массой возможностей, включая вывод среднего значения или информации со статистикой. Как пример, попробуем соединить данные, вот так:
String campPeople =  sport.stream().filter(p -> p.getName() != null).map(SportsCamp::getName).collect(Collectors.joining(" and ","In camp "," rest all days."));
System.out.println(campPeople);

Результат:«In camp Ivan and Petr and Ivan and Ira rest all days ». Есть несколько вариантов использования Collectors.joining.

Из Map, Flat и Reduce остановимся на примере с reduce. Map и flat-map будут рассмотрены в следующих статьях.
Reduce используется для «сборки» элементов, простым языком, если вы хотите в потоке произвести создание нового экземпляра объекта с агрегирующими показателями других элементов, то reduce вам подойдет. Существует несколько вариантов использования. Рассмотрим один из них, например, произведем суммирование данных по всем дням пребывания в спортивном лагере.
Integer daySum = sport.stream().reduce(0, (sum, p) -> sum += p.getDay(), (sum1, sum2) -> sum1 + sum2);
System.out.println("DaySize=" + daySum);

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

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

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


  1. StasTs
    06.06.2016 12:02

    Я конечно сейчас получу кучу минусов, но вот это:

    String campPeople = sport.stream().filter(p -> p.getName() != null).map(SportsCamp::getName).collect(Collectors.joining(" and ",«In camp »," rest all days."));

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


    1. AlexeyVD
      06.06.2016 12:37
      +11

      Если добавить немного форматирования, то всё становится не так уж плохо:

      String campPeople = sport.stream()
                      .filter(p -> p.getName() != null)
                      .map(SportsCamp::getName)
                      .collect(Collectors.joining(" and ",«In camp »," rest all days."));
      


      1. Terran37
        06.06.2016 14:34

        да, это удачное форматирование при разработке.


        1. Fen1kz
          06.06.2016 15:17
          +3

          Да и при написании/прочтении статьи тоже, если честно


    1. Timmmm
      06.06.2016 12:40
      +2

      Так по моему лучше

      String campPeople = sport.stream()
      .filter(Objects::nonNull)
      .map(SportsCamp::getName)
      .collect(Collectors.joining(" and ",«In camp »," rest all days."));
      
      Каждая строка описывает одну манипуляцию над стримом, что более очевидно передает логику.


      1. pullover
        06.06.2016 14:43
        +1

        Чуть подправлю:

        String campPeople = sport.stream()
        .map(SportsCamp::getName)
        .filter(Objects::nonNull)
        .collect(Collectors.joining(" and ",«In camp »," rest all days."));
        


        1. Timmmm
          06.06.2016 14:50

          Да, конечно вы правы.


    1. sshikov
      06.06.2016 21:08

      Ну, минусов-то тут не за что. Нормальный вопрос, на нормальную (хотя и далеко не новую тему).

      Попробую дать свой ответ.

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

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


  1. zzashpaupat
    06.06.2016 13:41
    +1

    Еще одна статья про Stream API, коих было уже предостаточно. Описано как-то очень сумбурно. Для людей, имеющих опыт работы со стримами — ничего нового, а для новичков — ничего не понятно.


  1. afanasiy_nikitin
    06.06.2016 20:57

    Я не даю четкого определения и точного размера коллекций

    ~10000 (из книги Р. Уорбертона «Лямбда-выражения в Java 8»)


    1. Terran37
      06.06.2016 21:04

      У Вас будет точная формулировка этой цифры из книги? Возможно были дополнительные условия применения именно 10000?


      1. afanasiy_nikitin
        08.06.2016 16:54
        -1

        Имеется в виду размер коллекции, при котором использование parallelStream действительно оправдано по производительности (поскольку, как известно, fork-join имеет некоторый оверхед на разделение/слияние потоков). Точную цитату могу поискать чуть позже. Ричард Уорбертон является одним из авторов-разработчиков данной технологии (и вообще Stream API), думаю, ему можно доверять. Книга небольшая, переведена на русский и есть в магазинах (и, к слову, в миллион раз полезнее, чем данная статья)


        1. Terran37
          08.06.2016 17:19
          -1

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


          1. grossws
            09.06.2016 04:05
            -1

            Про полезность статьи судить не Вам(возможно, что многим примеры будут полезны).


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


            1. Terran37
              09.06.2016 09:30

              Данная фраза относилась к пользователю afanasiy_nikitin. Мне кажется, что я обоснованно рассказал в комментариях ниже свою точку зрения. Еще раз повторю, что если мы заявляем о фактах из книг, то мы должны в этом быть уверены. Я не поленился и перечитал(уже почти полностью) данную книгу и написал, что afanasiy_nikitin не прав и в чем, привел выдержки из книги.
              Кстати, книгу всем советую, действительно отличная.


              1. grossws
                09.06.2016 20:09

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


        1. Terran37
          08.06.2016 17:48

          Не ищите цитату, т.к. вот она:
          «При замере времени работы примеров 6.1 и 6.2 на 4-ядерной машине при 10 альбомах последовательная версия оказывается в 8 раз
          быстрее. При 100 альбомах обе версии работают одинаково быстро,
          а при 10 000 альбомов параллельная версия опережает последовательную в 2,5 раза.
          Все результаты измерений в этой главе приводятся только для сведения. На вашей машине они могут оказаться совершенно другими.
          Размер входного потока – не единственный фактор, определяющий, даст ли распараллеливание ускорение. Результаты могут также
          зависеть от способа написания кода и количества доступных ядер.»
          Автор книги Ричард Уорбэртон приводит нам пример про 10000(не говоря о том, что 10000 — это идеальное число для «отсечки») и обратите внимание, что скорости могут отличаться.


          1. afanasiy_nikitin
            08.06.2016 18:49

            Речь идет о количестве порядков, я полагаю? Btw, я рад, что вы нашли книгу. Надеюсь, вы прочитаете ее целиком, и ваш код (и, может быть и эта статья) обогатится более точными и полезными примерами и комментариями. Спасибо за дискуссию.


            1. Terran37
              08.06.2016 22:18

              1) Речь про значение в 10000 идет о альбомах (музыкальных треках).
              2) Не могу понять Ваше желание сравнивать книги и статьи, приводя неправильные примеры, вводя в заблуждение читателей. Замечу. что автор книги(Ричард Уорбэртон), начиная со 107 страницы, подробно пишет о производительности при разных структурах и объемах данных. Вами сказанного значения в "~10000" у него нет.
              3)Пожалуйста.


    1. Terran37
      06.06.2016 21:37

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