У Java потік являє собою упорядковану послідовність елементів, над якою можливо виконувати операції, як послідовні, так і паралельні.
Кількість проміжних операцій може бути довільною, і завершується все термінальною операцією, котра повертає підсумковий результат.
Що ж таке потік?
Керувати потоками дозволяє Stream API, що з’явився у Java 8.
Спробуйте уявити собі потік як виробничу лінію, де вироби спочатку обробляють, потім сортують і наостанок готують до транспортування. У Java ці вироби – це об’єкти або їх колекції, операції – це обробка, сортування та пакування, а конвеєр – це потік.
Потік складається з наступних компонентів:
- Початкове джерело даних
- Проміжні перетворення
- Завершальна обробка
- Кінцевий результат
Розглянемо деякі ключові особливості потоків у Java:
- Потік не зберігає дані у пам’яті, він є послідовністю елементів масивів, об’єктів або колекцій, які обробляються спеціалізованими методами.
- Потоки використовують декларативний підхід – ви вказуєте *що* потрібно зробити, не визначаючи *як*.
- Потік можна використати тільки один раз, оскільки його результати не зберігаються.
- Потік не модифікує початкові дані, а створює на їх основі нову структуру.
- Потік повертає результат, отриманий внаслідок виконання завершальної операції.
Відмінності Stream API від обробки колекцій
Колекція — це структура даних у пам’яті, яка призначена для зберігання і обробки інформації. Колекції надають різноманітні структури, такі як Set, Map, List, для зберігання даних. Потік, натомість, це спосіб ефективної обробки і передачі даних через конвеєр.
Ось приклад роботи з колекцією ArrayList:
import java.util.ArrayList; public class Main { public static void main(String[] args) { ArrayList list = new ArrayList(); list.add(0, 3); System.out.println(list); } } Output: [3]
З прикладу видно, що можна створити ArrayList, додати до нього дані, а потім використовувати різні методи для маніпуляцій з ними.
Потік дозволяє обробляти існуючі структури даних і повертати нові, змінені значення. Нижче показано, як створити ArrayList та відфільтрувати його елементи за допомогою потоку.
import java.util.ArrayList; import java.util.stream.Stream; public class Main { public static void main(String[] args) { ArrayList<Integer> list = new ArrayList(); for (int i = 0; i < 20; i++) { list.add(i+1); } System.out.println(list); Stream<Integer> filtered = list.stream().filter(num -> num > 10); filtered.forEach(num -> System.out.println(num + " ")); } } #Output [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] 11 12 13 14 15 16 17 18 19 20
У наведеному прикладі потік формується з наявного списку, який потім використовується для фільтрації елементів, більших за 10. Важливо, що потік сам по собі не зберігає дані – він тільки обробляє елементи списку, виводячи результат. Спроба вивести потік на екран покаже посилання на об’єкт потоку, а не самі значення.
Робота з Java Stream API
Java Stream API приймає на вхід колекцію або послідовність елементів та виконує над ними операції, приводячи до кінцевого результату. Потік можна розглядати як конвеєр, через який проходять елементи, зазнаючи різноманітних перетворень.
Потік можна створити з різних джерел, зокрема:
- З колекцій, наприклад, List або Set.
- З масивів.
- З файлів та шляхів за допомогою буфера.
В обробці потоків виділяють два види операцій:
- Проміжні операції
- Термінальні операції
Проміжні та термінальні операції
Кожна проміжна операція повертає новий потік, котрий трансформує дані відповідно до вказаних параметрів. Фактична обробка не відбувається доти, доки не буде запущена термінальна операція. Лише тоді потік починає обробляти дані для отримання підсумкового результату.
Припустимо, у вас є список з 10 чисел, які потрібно спочатку відфільтрувати, а потім перетворити. Процес не полягає в тому, що кожен елемент буде миттєво відфільтрований і перетворений. Натомість кожен елемент буде перевірено на відповідність фільтру, і лише відповідні елементи зазнають подальших трансформацій. Кожен крок обробки створює новий потік для кожного елемента.
Операція перетворення (map) буде застосована до окремих елементів, які пройшли фільтр, а не до всього списку цілком. На завершення, термінальна операція об’єднає всі ці результати в єдиний вихід.
Після завершення термінальної операції потік вважається використаним і не може бути застосований повторно. Для повторного використання даних потрібно створити новий потік.
Джерело: The Bored Dev
Зараз, коли ми маємо базове розуміння потоків, перейдемо до розгляду їх реалізації в Java.
#1. Порожній потік
Порожній потік можна створити за допомогою методу `empty()` Stream API.
import java.util.stream.Stream; public class Main { public static void main(String[] args) { Stream emptyStream = Stream.empty(); System.out.println(emptyStream.count()); } } Output: 0
Висновок показує, що кількість елементів у порожньому потоці дорівнює 0. Порожні потоки зручні для уникнення помилок `NullPointerException`.
#2. Потік із колекцій
Колекції, такі як List та Set, надають метод `stream()`, що дозволяє створити потік на основі колекції. Після створення потік можна обробляти для отримання потрібного результату.
ArrayList<Integer> list = new ArrayList(); for (int i = 0; i < 20; i++) { list.add(i+1); } System.out.println(list); Stream<Integer> filtered = list.stream().filter(num -> num > 10); filtered.forEach(num -> System.out.println(num + " ")); #Output [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] 11 12 13 14 15 16 17 18 19 20
#3. Потік із масивів
Для створення потоку з масиву використовується метод `Arrays.stream()`.
import java.util.Arrays; public class Main { public static void main(String[] args) { String[] stringArray = new String[]{"this", "is", "techukraine.net"}; Arrays.stream(stringArray).forEach(item -> System.out.print(item + " ")); } } #Output this is techukraine.net
Також є можливість створити потік з частини масиву, вказавши початковий та кінцевий індекси елементів. Початковий індекс входить в діапазон, а кінцевий – ні.
String[] stringArray = new String[]{"this", "is", "techukraine.net"}; Arrays.stream(stringArray, 1, 3).forEach(item -> System.out.print(item + " ")); Output: is techukraine.net
#4. Пошук мінімальних і максимальних чисел за допомогою потоків
Для знаходження максимального та мінімального елемента в колекції або масиві використовуються компаратори. Методи `min()` та `max()` приймають компаратор та повертають `Optional` об’єкт.
`Optional` об’єкт – це контейнер, який може або містити, або не містити значення. Якщо значення наявне, то метод `get()` поверне його.
import java.util.Arrays; import java.util.Optional; public class MinMax { public static void main(String[] args) { Integer[] numbers = new Integer[]{21, 82, 41, 9, 62, 3, 11}; Optional<Integer> maxValue = Arrays.stream(numbers).max(Integer::compare); System.out.println(maxValue.get()); Optional<Integer> minValue = Arrays.stream(numbers).min(Integer::compare); System.out.println(minValue.get()); } } #Output 82 3
Ресурси для навчання
Тепер, коли ви отримали базове розуміння про потоки в Java, пропонуємо 5 джерел, що допоможуть вам глибше освоїти Java 8:
#1. Java 8 в дії
Ця книга є детальним керівництвом з нових можливостей Java 8, включаючи потоки, лямбда-вирази та функціональний стиль програмування. Книга також містить тести та питання для самоперевірки.
Ви можете придбати книгу в м’якій обкладинці та в аудіоформаті на Amazon.
#2. Java 8 Lambda: Функціональне програмування для мас
Ця книга спеціально розроблена для розробників Java SE, щоб пояснити, як саме використання лямбда-виразів впливає на мову Java. Вона містить зрозумілі пояснення, приклади коду та вправи для закріплення матеріалу.
Книга доступна у м’якій обкладинці та у форматі Kindle на Amazon.
#3. Java SE 8 для справді нетерплячих
Якщо ви є досвідченим розробником Java SE, ця книга проведе вас через нововведення Java SE 8, включаючи потоковий API, лямбда-вирази, покращення для паралельного програмування та деякі можливості Java 7, про які більшість розробників не знають.
Книга доступна тільки в м’якій обкладинці на Amazon.
#4. Вивчіть функціональне програмування на Java з лямбда-виразами та потоками
Цей курс від Udemy висвітлює основи функціонального програмування в Java 8 та 9. Лямбда-вирази, посилання на методи, потоки та функціональні інтерфейси – це основні теми курсу.
Курс також містить набір головоломок та вправ, пов’язаних із функціональним програмуванням.
#5. Бібліотека класів Java
Бібліотека класів Java є частиною спеціалізації Java, що пропонується Coursera. Курс навчить вас писати безпечний код за допомогою Java Generics, розуміти бібліотеку класів, що містить понад 4000 класів, працювати з файлами та обробляти помилки під час виконання. Існують деякі передумови для проходження цього курсу:
- Вступ до Java
- Вступ до об’єктно-орієнтованого програмування в Java
- Об’єктно-орієнтовані ієрархії в Java
На завершення
Java Stream API та введення лямбда-виразів у Java 8 значно спростили та покращили багато аспектів в Java, наприклад, паралельну обробку даних, використання функціональних інтерфейсів, зменшення об’єму коду тощо.
Проте потоки мають певні обмеження, їх головний недолік – це одноразове використання. Якщо ви є Java розробником, згадані ресурси допоможуть вам розібратися з цими темами детальніше, тому не зволікайте, вивчіть їх.
Також не забудьте вивчити основи обробки винятків у Java.