Более четкое, удобочитаемое и мощное кодирование с помощью Java SE 8 Streams… ..
В этой статье серии «Тур по новым возможностям Java SE 8» мы углубимся в объяснение и изучение кода о том, как обходить коллекции с помощью потоков. Создание потоков из коллекций и массивов, Агрегирование потоковых значений.
В предыдущей статье « Обход, фильтрация, обработка сбора и улучшения методов с помощью Lambda »; Я глубоко погрузился в объяснение и исследование того, как обходить коллекции с использованием лямбда-выражения и ссылок на методы , фильтровать их с помощью интерфейса предикатов , реализовывать методы по умолчанию в интерфейсах и, наконец, реализовывать статические методы в интерфейсах.
- Исходный код размещен на моей учетной записи Github : клонируйте его отсюда .
Содержание
- Обход коллекций с потоками.
- Создание потоков из коллекций и массивов.
- Агрегирование потоковых значений.
1. Обход коллекций с потоками
Вступление
Инфраструктура коллекций Java позволяет легко управлять упорядоченными и неупорядоченными коллекциями данных в ваших приложениях, используя интерфейсы, такие как List
и Map
, и классы, такие как Arraylist
и HashMap
. Структура коллекции постоянно развивалась с момента ее первого представления. А в Java SE 8 у нас теперь есть новый способ управления , обхода и объединения коллекций с помощью потокового API. Поток на основе коллекции не похож на поток ввода или вывода.
Как это работает
Вместо этого это новый способ работы с данными в целом, а не с каждым элементом в отдельности. Когда вы используете потоки, вам не нужно беспокоиться о деталях зацикливания или обхода. Вы создаете объект потока прямо из коллекции. И тогда вы можете делать с ним все, включая обход, фильтрацию и агрегирование его значений. Я начну с этого примера в пакете eg.com.tm.java8.features.stream.traversing
проекта Java8Features
. В коде класса SequentialStream
в Java SE 8 есть два вида потоков сбора, известных как последовательные и параллельные потоки.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
List<person> people = new ArrayList<>(); people.add( new Person( "Mohamed" , 69 )); people.add( new Person( "Doaa" , 25 )); people.add( new Person( "Malik" , 6 )); Predicate<person> pred = (p) -> p.getAge() > 65 ; displayPeople(people, pred); ........... private static void displayPeople(List<person> people, Predicate<person> pred) { System.out.println( "Selected:" ); people.forEach(p -> { if (pred.test(p)) { System.out.println(p.getName()); } }); } |
Последовательный поток является более простым из двух и точно так же, как итератор, он позволяет вам работать с каждым элементом в коллекции по одному. Но с меньшим синтаксисом, чем раньше. В этом коде я создал массив людей, приведенный в виде списка. И у него есть три экземпляра сложного объекта, класса с именем Person
. Затем я использую Predicate
для объявления условия и показываю людей, которые только удовлетворяют условию. От строк 48 до 52 в displayPeople()
я просматриваю коллекцию, перебираю данные и проверяю каждый элемент по одному. запустите код, и вы должны получить следующие результаты:
1
2
|
Selected: Mohamed |
Я покажу вам, как перефакторинг этого кода с помощью объекта потока. Сначала я закомментирую эти строки кода. Теперь, под комментарием к коду, я начну с объекта коллекции. Люди. И тогда я вызову новый метод под названием stream
. Потоковый объект, как и сама коллекция, имеет общее объявление. Если вы получаете поток из коллекции, элементы в потоке того же типа, что и сама коллекция. В моей коллекции есть экземпляры класса person, поэтому в потоке используется один и тот же универсальный тип.
1
2
3
4
5
6
7
8
9
|
System.out.println( "Selected:" ); // people.forEach(p -> { // if (pred.test(p)) { // System.out.println(p.getName()); // } // }); people.stream().forEach(p -> System.out.println(p.getName())); } |
Вы вызываете stream как метод, и теперь у вас есть объект stream, с которым вы можете что-то делать. Я начну с простого вызова каждого из четырех методов, и для этого потребуется выражение Lamda. Я передам в споре. Это пункт в списке, с которым я сейчас имею дело в течение итерации. Затем Лямбда-оператор, а затем реализация метода. И я буду использовать простой вывод системы, и я выведу имя человека. Я буду сохранять и запускать код, и результат будет. Поскольку я больше не фильтрую, я отображаю всех людей в списке.
1
2
3
4
|
Selected: Mohamed Doaa Malik |
Теперь, когда у вас есть поток, вы сможете легко использовать объект предиката. Когда я использую для каждого метода и имел дело с каждым пунктом по одному. Я должен был явно вызвать метод теста предиката. Но используя поток, вы можете вызвать метод с именем filter. Это ожидает объект предиката, и все предикаты имеют тестовый метод, и поэтому он уже знает, как вызвать этот метод. Итак, я немного разбью этот код. Я перенесу вызов .forEach()
на пару строк вниз, а затем на пустую строку посередине вызову новый метод фильтра.
1
2
3
|
people.stream() .filter(pred) .forEach(p -> System.out.println(p.getName())); |
Метод фильтра ожидает экземпляр интерфейса предиката. И я передам свой объект предиката. Метод filter возвращает поток, но теперь отфильтрованную версию, и оттуда я могу вызвать метод forEach()
. Я выполню код, и теперь я отображаю только элементы из коллекции, которые удовлетворяют условию предиката. Вы можете сделать намного больше с потоками. Посмотрите документацию по потокам в документации по Java SE 8 API.
1
2
|
Selected: Mohamed |
И вы увидите, что в дополнение к фильтрации вы также можете собирать и выполнять другие виды операций с потоками. Прежде чем закончить эту демонстрацию, я хочу показать вам очень важное различие между последовательным и параллельным потоками. Одна из целей потокового API в Java SE 8 — дать вам возможность разбить обработку в системе с несколькими ЦП. Эта многопроцессорная обработка автоматически выполняется средой выполнения Java. Все, что вам нужно сделать, это превратить ваш последовательный поток в параллельный поток.
И есть несколько способов сделать это синтаксически. Я сделаю копию моего последовательного потокового класса. Я пойду к своему исследователю пакетов, и я скопирую это и вставлю это. И я назову новый класс, ParallelStream
. И я открою новый класс. В этой версии я избавлюсь от закомментированного кода. Мне это больше не нужно. А теперь вот два способа создания параллельного потока. Одним из подходов является вызов другого метода из коллекции. Вместо потока я буду вызывать parallelStream()
. И теперь у меня есть поток, который будет автоматически разбит на несколько процессоров.
1
2
3
4
5
6
|
private static void displayPeople(List<person> people, Predicate<person> pred) { System.out.println( "Selected:" ); people.parallelStream() .filter(pred) .forEach(p -> System.out.println(p.getName())); } |
Я запускаю код и вижу, что он делает то же самое, фильтруя и возвращая данные.
1
2
|
Selected: Mohamed |
Вот другой способ создать параллельный поток. Я вызову этот метод stream()
снова. А затем из потокового метода я вызову метод с именем parallel()
и он делает то же самое. Я начинаю с последовательного потока и заканчиваю параллельным потоком. Это все еще поток. Он все еще может фильтровать, он может обрабатывать точно так же, как и раньше. Но теперь это будет разбито, где это возможно.
1
2
3
4
|
people.stream() .parallel() .filter(pred) .forEach(p -> System.out.println(p.getName())); |
Вывод
Нет четкого предписания, когда использовать параллельный поток поверх последовательного. Это зависит от размера и сложности ваших данных, а также от возможностей оборудования. Многопроцессорная система, на которой вы работаете. Единственная рекомендация, которую я могу вам дать, — это попробовать ее с вашим приложением и вашими данными. Установите ориентиры, рассчитайте время выполнения операции. Используйте последовательный поток и используйте параллельный поток и посмотрите, что работает лучше для вас.
2. Создание потоков из коллекций и массивов
Вступление
Потоковый API Java SE 8 разработан для того, чтобы помочь вам управлять коллекциями данных, то есть объектами, которые являются членами структуры коллекции, такими как списки массивов или карта хешей. Но вы также можете создавать потоки прямо из массивов.
Как это работает
В этом проекте Java8Features
в пакете eg.com.tm.java8.features.stream.creating
меня есть класс с именем ArrayToStream
. И в его основном методе я создал массив из трех элементов. И каждый из них является экземпляром моего сложного объекта, класса Person
.
01
02
03
04
05
06
07
08
09
10
|
public static void main(String args[]) { Person[] people = { new Person( "Mohamed" , 69 ), new Person( "Doaa" , 25 ), new Person( "Malik" , 6 )}; for ( int i = 0 ; i < people.length; i++) { System.out.println(people[i].getInfo()); } } |
Этот класс имеет сеттеры и геттеры для приватных полей и новый getInfo()
для возврата объединенной строки.
1
2
3
|
public String getInfo() { return name + " (" + age + ")" ; } |
Теперь, если вы хотите использовать поток для обработки этого массива, вы можете подумать, что вам нужно будет преобразовать его в список массивов, возможно, затем создать поток. Но оказывается, что есть пара способов перейти непосредственно из массива в поток. Вот первый подход. Мне не понадобятся эти три строки кода, которые я использую для обработки данных. Я прокомментирую это. И затем здесь, я объявлю объект для типа является потоком.
Stream
— это интерфейс, который является членом java.util.stream
. Когда я нажимаю Ctrl + Пробел и выбираю его из списка, меня просят указать общий тип элементов, которыми будет управлять поток. И это будут элементы типа Person
, как и элементы в самом массиве. Я назову мой новый объект потока, поток, в нижнем регистре. И вот первый способ создать поток. Снова используйте интерфейс потока и вызовите метод с именем of()
. Обратите внимание, что есть несколько разных версий.
Тот, который принимает один объект, и тот, который принимает ряд объектов. Я буду использовать тот, который принимает один аргумент, и я передам в мой массив people
, и это все, что мне нужно сделать. Stream.of()
означает взять этот массив и обернуть его внутри потока. И теперь я могу использовать лямбда-выражения, фильтры, ссылки на методы и другие вещи, которые работают с объектами Stream. Я буду вызывать объекты потока для каждого метода, и я передам лямбда-выражение, я передам имя текущего человека, а затем, после оператора лямбды, выведу информацию о человеке. Использование объекта getInfo()
.
01
02
03
04
05
06
07
08
09
10
|
Person[] people = { new Person( "Mohamed" , 69 ), new Person( "Doaa" , 25 ), new Person( "Malik" , 6 )}; // for (int i = 0; i < people.length; i++) { // System.out.println(people[i].getInfo()); // } Stream<Person> stream = Stream.of(people); stream.forEach(p -> System.out.println(p.getInfo())); |
Я буду сохранять и запускать код, и результат будет. Я выводю элементы в том же порядке, в котором они были размещены в массиве. Итак, это один из подходов с использованием Stream.of()
.
1
2
3
|
Mohamed ( 69 ) Doaa ( 25 ) Malik ( 6 ) |
Есть другой подход, который делает то же самое. Я собираюсь продублировать эту строку кода и закомментировать одну версию. И на этот раз при использовании Stream.of()
я буду использовать класс с именем Arrays
, который является членом пакета java.util
.
И оттуда я вызову метод с именем stream. Обратите внимание, что потоковый метод может быть обернут вокруг массивов различных типов. Включая как примитивы, так и сложные объекты.
1
2
3
4
|
// Stream<person> stream = Stream.of(people); Stream<person> stream = Arrays.stream(people); stream.forEach(p -> System.out.println(p.getInfo())); |
Я сохраню и запусту эту версию, и поток будет делать то же самое, что и раньше.
1
2
3
|
Mohamed ( 69 ) Doaa ( 25 ) Malik ( 6 ) |
Вывод
Так что Stream.of()
или Arrays.stream()
будут делать то же самое. Возьмите массив примитивных значений или сложных объектов и превратите их в поток, который затем можно использовать с лямбдами, фильтрами и ссылками на методы.
3. Агрегирование потоковых значений
Вступление
Ранее я описывал, как использовать поток для перебора коллекции. Но вы также можете использовать потоки для объединения элементов в коллекции. То есть вычислять суммы , средние значения , подсчеты и так далее. Когда вы выполняете такую операцию, важно понимать природу параллельных потоков .
Как это работает
Поэтому я собираюсь начать эту демонстрацию в проекте Java8Features
, в пакете eg.com.tm.java8.features.stream.aggregating
. И я собираюсь сначала поработать с классом ParallelStreams
. В методе main
этого класса я создал список массивов, содержащий строковые элементы.
Я использую простой цикл, я добавил в список 10000 элементов. Затем в строках 35 и 36 я создаю поток и использую для каждого метода и выводю каждый поток по одному.
01
02
03
04
05
06
07
08
09
10
|
public static void main(String args[]) { System.out.println( "Creating list" ); List<string> strings = new ArrayList<>(); for ( int i = 0 ; i < 10000 ; i++) { strings.add( "Item " + i); } strings.stream() .forEach(str -> System.out.println(str)); } |
Когда я запускаю этот код, я получаю ожидаемый результат. Элементы выводятся на экран в том же порядке, в котором они были добавлены в список.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
......... Item 9982 Item 9983 Item 9984 Item 9985 Item 9986 Item 9987 Item 9988 Item 9989 Item 9990 Item 9991 Item 9992 Item 9993 Item 9994 Item 9995 Item 9996 Item 9997 Item 9998 Item 9999 |
Теперь давайте посмотрим, что происходит, когда мы превращаем это в параллельный поток. Как я описал ранее, я могу сделать это либо путем вызова метода параллельного потока, либо путем получения результатов потока и передачи их в параллель.
Я сделаю последнее. Сейчас я работаю с параллельным потоком, это поток, который можно разбить и распределить рабочую нагрузку между несколькими процессорами.
1
2
3
|
strings.stream() .parallel() .forEach(str -> System.out.println(str)); |
Я еще раз выполню код и посмотрю, что произойдет, заметив, что последний напечатанный элемент — не последний элемент в списке. Это было бы 9,999. А если прокрутить вывод, я увижу, что обработка каким-то образом прыгает. Происходит то, что во время выполнения данные произвольно разбиваются на блоки.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
......... Item 5292 Item 5293 Item 5294 Item 5295 Item 5296 Item 5297 Item 5298 Item 5299 Item 5300 Item 5301 Item 5302 Item 5303 Item 5304 Item 5305 Item 5306 Item 5307 Item 5308 Item 5309 Item 5310 Item 5311 |
И затем передача каждого блока доступному процессору. Только после того, как все блоки будут обработаны, мой следующий бит кода Java будет выполнен. Но внутри, внутри вызова метода forEach()
, вся эта работа разделяется по мере необходимости. Теперь это может или не может обеспечить выигрыш в производительности. Это зависит от размера вашего набора данных. И характер вашего оборудования. Но одна из вещей, которую показывает этот пример, заключается в том, что если вам нужно обрабатывать элементы последовательно, то есть по одному в том же порядке, в котором они были добавлены в коллекцию, то параллельный поток может оказаться не лучшим способом. Это.
Последовательные потоки могут гарантировать, что они работают в одном и том же порядке каждый раз. Но параллельный поток, по определению, будет действовать наиболее эффективным способом. Поэтому параллельные потоки особенно полезны, когда вы агрегируете операции. Где вы принимаете во внимание все элементы в коллекции, а затем создаете какую-то совокупную стоимость из них. Я покажу вам примеры подсчета предметов в коллекции, их усреднения и суммирования с использованием строк.
В этом классе CountItems
в методе main я начинаю с того же базового кода. Создание 10000 строк в списке. И затем есть для каждого метода, который проходит через и обрабатывает их по одному.
01
02
03
04
05
06
07
08
09
10
|
public static void main(String args[]) { System.out.println( "Creating list" ); List<string> strings = new ArrayList<>(); for ( int i = 0 ; i < 10000 ; i++) { strings.add( "Item " + i); } strings.stream() .forEach(str -> System.out.println(str)); } |
В этом примере вместо обработки каждой строки по отдельности, я хочу подсчитать их. Итак, я закомментирую этот код, и вот код, который я буду использовать. Поскольку я не знаю точно, сколько предметов будет в коллекции. Я обналичу результат, который я собираюсь создать, как длинное целое.
И я назову это count
, и я получу его значения, вызвав strings
. Это моя коллекция .stream()
, .count()
, и это возвращает длинное значение. Затем я использую вывод системы и сообщу о результате. С подсчетом: и тогда я добавлю свой результат.
1
2
3
4
|
// strings.stream() // .forEach(str -> System.out.println(str)); long count = strings.stream().count(); System.out.println( "Count: " + count); |
Я сохраню свои изменения и выполню код, и вот результат. Количество предметов в коллекции практически мгновенно.
1
2
|
Creating list Count: 10000 |
Теперь, чтобы сделать это немного более драматичным, я добавлю пару нулей здесь, и теперь я имею дело с 1 000 000 000 строк. Я снова запускаю код, и результат возвращается почти мгновенно.
1
2
|
Creating list Count: 1000000 |
Теперь посмотрим, что произойдет, если я вместо этого распараллелю строку. Я добавлю точку параллели здесь:
1
2
3
4
|
// strings.stream() // .forEach(str -> System.out.println(str)); long count = strings.stream().parallel().count(); System.out.println( "Count: " + count); |
А потом я запусту код, и это займет немного больше времени. Теперь я могу измерить, сколько времени занимает выполнение этих операций, фиксируя текущую отметку времени до и после операции. А потом немного по математике. И то, что это покажет, может отличаться от одной системы к другой. Но по моему опыту при работе с такими видами простых коллекций, содержащих простые значения, параллельные потоки не приносят большой пользы. Ваш пробег может очень, хотя. И я призываю вас сделать свой собственный сравнительный анализ. Но вот как бы вы сделали подсчет.
Давайте посмотрим на суммирование и усреднение . Я пойду в свой класс SumAndAverage
. На этот раз у меня есть список объектов из трех человек, каждый из которых имеет свой возраст. И моя цель — получить сумму трех возрастов и среднее значение трех возрастов. Я добавлю новую строку кода после того, как все экземпляры класса person будут добавлены в список. И я создам целочисленную переменную, которую назову sum
.
Я начну с получения потока, используя people.stream().
Оттуда я буду вызывать метод с именем mapToInt()
. Обратите внимание, что есть метод карты. mapToDouble()
и mapToLong()
также. Цель этих методов — взять сложный объект и извлечь из него простое примитивное значение, создать поток этих значений, и вы делаете это с помощью лямбда-выражения. Итак, я выберу mapToInt()
потому что возраст каждого человека — целые числа.
Для лямбда-выражения я начну с переменной, которая будет представлять текущего человека. Затем оператор Lambda, а затем выражение, которое возвращает целое число. Я буду использовать p.getAge()
. Это возвращает то, что называется строкой int или строкой целых чисел. Есть также класс двойной строки и несколько других. Теперь из этого потока, поскольку я уже знал, что это числовое значение, я могу вызвать метод sum()
. Вот и все. Теперь я суммировал все старые значения из всех личных объектов в моей коллекции. С помощью одного оператора я выведу результат с помощью System Output. Мой лейбл будет длиться годами, и я добавлю к этому мою сумму.
1
2
3
4
5
6
7
8
9
|
List<person> people = new ArrayList<>(); people.add( new Person( "Mohamed" , 69 )); people.add( new Person( "Doaa" , 25 )); people.add( new Person( "Malik" , 6 )); int sum = people.stream() .mapToInt(p -> p.getAge()) .sum(); System.out.println( "Total of ages " + sum); |
Я сохраню свой код и запускаю его. И общее количество всех трех возрастов составляет 100.
1
|
Total of ages 100 |
Усреднение этих значений очень похоже. Но потому что всякий раз, когда вы делаете усреднение ваших делений деления, вы можете получить проблему деления на ноль, и поэтому, когда вы делаете усреднение, вы получите нечто, называемое Optional
переменной.
И есть несколько типов, которые вы можете использовать для этого. Для моего усреднения я собираюсь ожидать возвращения двойного значения. Итак, я собираюсь создать переменную с именем OptionalDouble. Обратите внимание, что есть также Optional Int и Optional Log. Я назову свою переменную Avg
, в среднем. И я буду использовать такой же код, который только что использовал, чтобы получить сумму, начиная с people.stream()
. А затем оттуда я снова буду использовать mapToInt()
. И я передам то же самое лямбда-выражение, которое я использовал в прошлый раз, а затем я вызову метод усреднения.
Теперь с объектом OptionalDouble
перед обработкой вы всегда должны убедиться, что он действительно имеет двойное значение, и вы делаете это с помощью метода isPresent()
. Итак, я начну с шаблона кода if else. И я установлю свое состояние на avg.isPresent()
. Если это условие верно, я буду использовать System Output. И я обозначу это просто как Среднее. И я добавлю свою среднюю переменную. В остальном я просто скажу, что среднее значение не было рассчитано.
1
2
3
4
5
6
7
8
|
OptionalDouble avg = people.stream() .mapToInt(p -> p.getAge()) .average(); if (avg.isPresent()) { System.out.println( "Average: " + avg); } else { System.out.println( "average wasn't calculated" ); } |
Теперь в этом примере я знаю, что он будет успешным, потому что я предоставил возраст всем трем людям, но это не всегда так. Как я уже сказал, если в результате вы получите ситуацию деления на ноль, вы не сможете получить двойное значение. Я сохраню и выполню код, и заметлю, что с необязательным двойным классом это сложный объект.
1
2
|
Total of ages 100 Average: OptionalDouble[ 33.333333333333336 ] |
Таким образом, тип обернут вокруг фактического значения. Я перейду к этому коду, где я getAsDouble()
на объект напрямую, и я назову его getAsDouble()
.
1
2
3
4
5
|
if (avg.isPresent()) { System.out.println( "Average: " + avg.getAsDouble()); } else { System.out.println( "average wasn't calculated" ); } |
А теперь я вернусь к примитивному двойному значению. Я снова запускаю код, и теперь результат — то, что я искал.
1
2
|
Total of ages 100 Average: 33.333333333333336 |
Вывод
Таким образом, используя потоки и лямбда-выражения, вы можете легко вычислить агрегированные значения из коллекций с помощью небольшого количества кода.
Ресурсы
- Учебные руководства Java, Агрегатные Операции
- API интерфейса Java Stream
- Учебники по Java, лямбда-выражения
- JSR 310: API даты и времени
- JSR 337: Java SE 8 Release Содержание
- Сайт OpenJDK
- Платформа Java, стандартное издание 8, спецификация API
Надеюсь, вам понравилось читать, так как мне понравилось писать, пожалуйста, поделитесь, если вам нравится, распространите слово.
Ссылка: | Обзор новых функций Java SE 8: обработка коллекций с помощью Streams API от нашего партнера по JCG Мохамеда Тамана в блоге « Улучшите свою жизнь с помощью науки и искусства» . |