Небольшая статья с примерами использования 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());
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);
Integer daySum = sport.stream().reduce(0, (sum, p) -> sum += p.getDay(), (sum1, sum2) -> sum1 + sum2);
System.out.println("DaySize=" + daySum);
В это варианте reduce принимает три значения, первый – идентификатора, второй – аккумулятор, а третий это – фактически «объедение». Существуют и несколько других вариантов.
В статье описана лишь небольшая часть методов, возможно, они заинтересуют многих и появятся в новых проектах. В совокупности лямбдами функционал становится отличным инструментом для написания краткого и быстроисполняемого кода. Всем удачи.
Автор: Terran37