JavaRush /Java блог /Random UA /Кава-брейк #177. Детальний посібник з Java Stream в Java ...

Кава-брейк #177. Детальний посібник з Java Stream в Java 8

Стаття з групи Random UA
Джерело: Hackernoon У цій публікації викладено докладний посібник з роботи з Java Stream разом із прикладами коду та поясненнями. Кава-брейк #177.  Детальний посібник з Java Stream в Java 8 - 1

Введення в потоки Java в Java 8

Потоки Java (Streams), що представлені як частина Java 8, використовуються для роботи з колекціями даних. Самі собою вони не є структурою даних, але можуть використовуватися для введення інформації з інших структур даних шляхом упорядкування та конвеєрної обробки для отримання остаточного результату. Примітка: Важливо не плутати Stream та Thread, оскільки в російській мові обидва терміни часто згадуються в однаковому перекладі “потік”. Stream позначає об'єкт виконання операцій (найчастіше передача даних чи його накопичення), тоді як Thread (дослівний переклад — нитка) позначає об'єкт, який дозволяє виконати певний програмний код паралельно коїться з іншими гілками коду. Оскільки Stream — це окрема структура даних, вона будь-коли змінює джерело даних. Потоки Java мають такі особливості:
  1. Java Stream можна використовувати за допомогою пакета java.util.stream. Його можна імпортувати в скрипт за допомогою коду:

    import java.util.stream.* ;

    Використовуючи цей код, ми також можемо легко реалізувати кілька вбудованих функцій Java Stream.

  2. Java Stream може приймати вхідні дані з колекцій даних, таких як колекції та масиви Java.

  3. Java Stream не потребує зміни структури вхідних даних.

  4. Java Stream не змінює джерело. Натомість він генерує вихідні дані за допомогою відповідних конвеєрних методів.

  5. Java Stream піддаються проміжним та термінальним операціям, які ми обговоримо у наступних розділах.

  6. У Java Stream проміжні операції конвеєризовані та відбуваються у форматі відкладених чи “лінивих” обчислень (lazy evaluation). Вони завершуються термінальними функціями. Це формує базовий формат використання Java Stream.

У наступному розділі ми розглянемо різні способи, що використовуються Java 8 для створення Java Stream в міру необхідності.

Створення Java Stream у Java 8

Потоки Java можна створювати кількома способами:

1. Створення порожнього потоку з допомогою методу Stream.empty()

Можна створити порожній потік для використання на пізніших етапах коду. Якщо використовувати метод Stream.empty() , то буде згенеровано порожній потік, який не містить значень. Цей порожній потік може стати в нагоді, якщо ми хочемо пропустити виключення нульового покажчика під час виконання. Для цього можна використати таку команду:
Stream<String> str = Stream.empty();
Наведений вище оператор згенерує порожній потік з ім'ям str без будь-яких елементів усередині нього. Щоб переконатися в цьому, достатньо перевірити кількість або розмір потоку за допомогою терміна str.count() . Наприклад,
System.out.println(str.count());
В результаті отримаємо на виведенні 0 .

2. Створення потоку за допомогою методу Stream.builder() з екземпляром Stream.Builder

Ми також можемо використовувати Stream Builder для створення потоку за допомогою шаблону проектування Builder. Він призначений для покрокової побудови об'єктів. Погляньмо, як ми можемо створити екземпляр потоку за допомогою Stream Builder .
Stream.Builder<Integer> numBuilder = Stream.builder();

numBuilder.add(1).add(2).add( 3);

Stream<Integer> numStream = numBuilder.build();
Використовуючи цей код, можна створити потік з ім'ям numStream , що містить елементи int . Все виконується досить швидко завдяки екземпляру Stream.Builder під ім'ям numBuilder , який створюється першим.

3. Створення потоку із зазначеними значеннями за допомогою методу Stream.of()

Ще один спосіб створення потоку передбачає використання методу Stream.of() . Це простий спосіб створення потоку із заданими значеннями. Він оголошує, і навіть ініціалізує потік. Приклад використання методу Stream.of() для створення потоку:
Stream<Integer> numStream = Stream.of(1, 2, 3);
Цей код створить потік, що містить елементи int як ми вже зробабо в попередньому методі з використанням Stream.Builder . Тут ми безпосередньо створабо потік, використовуючи Stream.of() із заздалегідь заданими значеннями [1, 2 та 3] .

4. Створення потоку з існуючого масиву за допомогою методу Arrays.stream()

Інший поширений метод створення потоку передбачає використання масивів Java. Потік створюється з існуючого масиву за допомогою методу Arrays.stream() . Усі елементи масиву перетворюються на елементи потоку. Ось наочний приклад:
Integer[] arr = {1, 2, 3, 4, 5};

Stream<Integer> numStream = Arrays.stream(arr);
Цей код буде генерувати потік numStream , що містить вміст масиву з ім'ям arr, який являє собою цілий масив (integer array).

5. Об'єднання двох існуючих потоків за допомогою методу Stream.concat()

Ще один метод, який можна використовувати для створення потоку, це метод Stream.concat() . Він використовується для поєднання двох потоків з метою створення єдиного потоку. Обидва потоки поєднуються по порядку. Це означає, що першим йде перший потік, за яким слідує другий потік, і так далі. Приклад такої конкатенації має такий вигляд:
Stream<Integer> numStream1 = Stream.of(1, 2, 3, 4, 5);

Stream<Integer> numStream2 = Stream.of(1, 2, 3);

Stream<Integer> combinedStream = Stream.concat( numStream1, numStream2);
Наведений вище оператор створить остаточний потік з ім'ям combinedStream , що містить один за одним елементи першого потоку numStream1 та другого потоку numStream2 .

Типи операцій із Java Stream

Як згадувалося, з Java Stream в Java 8 можна виконувати два типи операцій: проміжні (intermediate) і термінальні (terminal). Розглянемо кожну їх докладніше.

Проміжні операції

Проміжні операції генерують вихідний потік (output stream) та виконуються тільки при зустрічі з термінальною операцією. Це означає, що проміжні операції виконуються "ліниво", конвеєрно і можуть бути завершені лише термінальною операцією. Про ліниве обчислення та конвеєрну обробку ви дізнаєтеся трохи пізніше. Прикладами проміжних операцій є такі методи: filter() , map() , different() , peek() , sorted() та деякі інші.

Термінальні операції

Термінальні операції завершують виконання проміжних операцій, і навіть повертають остаточні результати вихідного потоку. Оскільки термінальні операції сигналізують про завершення лінивого виконання та конвеєрної обробки, цей потік не можна використовувати знову після того, як він зазнав термінальної операції. Прикладами термінальних операцій є такі методи: forEach() , collect() , count() , reduce() тощо.

Приклади операцій із Java Stream

Проміжні операції

Ось кілька прикладів деяких проміжних операцій, які можна застосовувати до Java Stream:

filter()

Цей метод використовується для фільтрації елементів з потоку, які відповідають певному предикату Java. Потім ці фільтровані елементи становлять новий потік. Погляньмо на приклад, щоб краще зрозуміти краще.
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> even = numStream.filter(n -> n % 2 == 0) .collect(Collectors.toList()); System.out.println(even);
Висновок:
[98]
Пояснення: У цьому прикладі видно, що парні елементи (діляться на 2) фільтруються за допомогою методу filter() і зберігаються в списку numStream , зміст якого друкується пізніше. Оскільки 98 - єдине парне ціле число потоці, воно друкується у висновку.

map()

Цей метод використовується створення нового потоку шляхом виконання зіставлених функцій над елементами вихідного вхідного потоку. Можливо новий потік має інший тип даних. Приклад виглядає так:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> d = numStream.map(n -> n*2) .collect(Collectors.toList()); System.out.println(d);
Висновок:
[86, 130, 2, 196, 126]
Пояснення: Тут бачимо, що метод map() використовується простого подвоєння кожного елемента потоку numStream . Як очевидно з висновку, кожен із елементів у потоці успішно подвоєний.

distinct()

Цей метод використовується для отримання лише окремих елементів у потоці шляхом фільтрації дублікатів. Приклад того ж виглядає так:
Stream<Integer> numStream = Stream.of(43,65,1,98,63,63,1); List<Integer> numList = numStream.distinct() .collect(Collectors.toList()); System.out.println(numList);
Висновок:
[43, 65, 1, 98, 63]
Пояснення: У цьому випадку numStream використовується метод different() . Він отримує всі окремі елементи в списку numList шляхом видалення дублікатів з потоку. Як видно з вивідних даних, дублікатів немає, на відміну від вхідного потоку, який спочатку мав два дублікати (63 та 1).

peek()

Цей метод використовується для відстеження проміжних змін перед виконанням термінальної операції. Це означає, що peek() можна використовувати для виконання операції над кожним елементом потоку для створення потоку, над яким можуть виконуватись подальші проміжні операції.
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> nList = numStream.map(n -> n*10) .peek(n->System.out.println("Mapped: "+ n)) .collect(Collectors.toList()); System.out.println(nList);
Висновок:
Mapped: 430 Mapped: 650 Mapped: 10 Mapped: 980 Mapped: 630 [430, 650, 10, 980, 630]
Пояснення: Тут метод peek() використовується для створення проміжних результатів, оскільки метод map() застосовується до елементів потоку. Тут ми можемо помітити, що навіть до застосування термінальної операції collect() для друку остаточного вмісту списку оператора print результат для кожного зіставлення елемента потоку друкується послідовно заздалегідь.

sorted()

Метод sorted() використовується сортування елементів потоку. За умовчанням він сортує елементи у порядку зростання. Також можна вказати конкретний порядок сортування як параметр. Приклад реалізації цього методу виглядає так:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); numStream.sorted().forEach(n -> System.out.println(n));
Висновок:
1 43 ​​63 65 98
Пояснення: Тут метод sorted() використовується для сортування елементів потоку в порядку зростання за умовчанням (оскільки конкретний порядок не вказано). Можна побачити, що елементи, надруковані у виводі, упорядковані у порядку, що зростає.

Термінальні операції

Ось кілька прикладів деяких термінальних операцій, які можна застосовувати до потоків Java:

forEach()

Метод forEach() використовується для перебору всіх елементів потоку та виконання функції для кожного елемента один за одним. Це діє як альтернатива операторам циклу, таким як for , while та іншим. Приклад:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); numStream.forEach(n -> System.out.println(n));
Висновок:
43 65 1 98 63
Пояснення: Тут метод forEach() використовується для друку кожного елемента потоку один за одним.

count()

Метод count() використовується для отримання загальної кількості елементів, присутніх у потоці. Він нагадує метод size() , який часто використовується визначення загальної кількості елементів у колекції. Приклад використання методу count() з Java Stream виглядає так:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); System.out.println(numStream.count());
Висновок:
5
Пояснення: Оскільки потік numStream містить 5 цілих елементів, то при використанні для нього методу count() висновок буде 5.

collect()

Метод collect() використовується для виконання змінних скорочень елементів потоку. Його можна використовувати для видалення вмісту потоку після завершення обробки. Для проведення редукцій він використовує клас Collector .
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); List<Integer> odd = numStream.filter(n -> n % 2 == 1) .collect(Collectors.toList()); System.out.println(odd);
Висновок:
[43, 65, 1, 63]
Пояснення: У цьому прикладі всі непарні елементи в потоці фільтруються і збираються/скорочуються до списку з ім'ям odd (непарні). Наприкінці друкується список непарних.

min() та max()

Метод min() , як випливає з назви, може використовуватися в потоці для пошуку мінімального елемента. Так само метод max() можна використовуватиме пошуку максимального елемента в потоці. Спробуймо зрозуміти, як їх можна використовувати, на прикладі:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); int smallest = numStream.min((m, n) -> Integer.compare(m, n)).get(); System.out.println("Smallest element: " + smallest);
numStream = Stream.of(43, 65, 1, 98, 63); int largest = numStream.max((m, n) -> Integer.compare(m, n)).get(); System.out.println("Largest element: " + largest);
Висновок:
Smallest element: 1 Largest element: 98
Пояснення: У цьому прикладі ми надрукували найменший елемент у потоці numStream за допомогою методу min() та найбільший елемент за допомогою методу max() . Зверніть увагу, що перед застосуванням методу max() ми знову додали елементи в потік numStream . Це пов'язано з тим, що min() є термінальною операцією і знищує вміст вихідного потоку, повертаючи лише остаточний результат (який у даному випадку був цілим “smallest”).

findAny() та findFirst()

findAny() повертає будь-який елемент потоку як Optional . Якщо потік порожній (empty), він поверне значення Optional , яке буде порожнім. findFirst() повертає перший елемент потоку як Optional . Як і у випадку з методом findAny() , findFirst() також повертає порожній параметр Optional , якщо відповідний потік порожній. Погляньмо на наступний приклад, заснований на цих методах:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); Optional<Integer> opt = numStream.findFirst();System.out.println(opt); numStream = Stream.empty(); opt = numStream.findAny();System.out.println(opt);
Висновок:
Optional[43] Optional.empty
Пояснення: Тут у першому випадку метод findFirst() повертає перший елемент потоку як Optional . Потім, коли потік перепризначається як порожній потік, findAny() метод повертає порожній Optional .

allMatch() , anyMatch() і noneMatch()

Метод allMatch() використовується для перевірки того, чи відповідають всі елементи в потоці певного предикату, і повертає логічне значення true якщо це так, в іншому випадку повертається false . Якщо потік порожній, він повертає true . Метод anyMatch() використовується для перевірки того, чи відповідає якийсь із елементів у потоці певному предикату. Він повертає true , якщо це так, і false інакше. Якщо потік порожній, він повертає false . Метод noneMatch() повертає true , якщо жоден елемент потоку не відповідає предикату, та falseв іншому випадку. Приклад, що ілюструє це, виглядає так:
Stream<Integer> numStream = Stream.of(43, 65, 1, 98, 63); boolean flag = numStream.allMatch(n -> n1); System.out.println(flag); numStream = Stream.of(43, 65, 1, 98, 63); flag = numStream.anyMatch(n -> n1); System.out.println(flag); numStream = Stream.of(43, 65, 1, 98, 63); flag = numStream.noneMatch(n -> n==1);System.out.println(flag);
Висновок:
false true false
Пояснення: Для потоку numStream , що містить 1 як елемент, метод allMatch() повертає false , тому що всі елементи не дорівнюють 1, а тільки один з них. Метод anyMatch() повертає true , оскільки хоча один із елементів дорівнює 1. Метод noneMatch() повертає false , оскільки 1 дійсно існує як елемент у цьому потоці.

Lazy Evaluations (ліниві обчислення) у Java Stream

Ліниві обчислення призводять до оптимізації при роботі з Java Streams в Java 8. Вони в основному пов'язані із затримкою виконання проміжних операцій доти, доки не зустрінеться термінальна операція. Ліниві обчислення відповідають за запобігання непотрібній витраті ресурсів на обчислення доти, доки результат справді не знадобиться. Вихідний потік, отриманий у результаті проміжних операцій, генерується лише після виконання термінальної операції. Ліниві обчислення працюють з усіма проміжними операціями в потоках Java. Дуже корисне застосування лінивих обчислень проявляється під час роботи з нескінченними потоками (infinite streams). У цьому випадку запобігає багато непотрібної обробки.

Конвеєри в Java Stream

Конвеєр (pipeline) в Java Stream включає вхідний потік, нуль або кілька проміжних операцій, збудованих одна за одною, і, нарешті, термінальну операцію. Проміжні операції в Java Streams виконуються "ліниво". Це робить конвеєрні проміжні операції неминучими. За допомогою конвеєрів, які в основному є проміжні операції, об'єднані по порядку, ліниве виконання стає можливим. Конвеєри допомагають відстежувати проміжні операції, які потрібно виконати після того, як нарешті зустрінеться термінальна операція.

Висновок

Давайте тепер підіб'ємо підсумок того, що ми сьогодні вивчабо. В цій статті:
  1. Ми коротко розглянули, що таке потоки Java (Java Stream).
  2. Потім ми дізналися про безліч різних методів створення потоків Java в Java 8.
  3. Ми вивчабо два основні типи операцій (проміжні операції та термінальні операції), які можна виконувати з потоками Java.
  4. Потім ми докладно розглянули кілька прикладів як проміжних, і термінальних операцій.
  5. У результаті ми докладніше дізналися про ліниві обчислення і, нарешті, вивчабо конвеєрну обробку в потоках Java.
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ