- Java 8 Stream API: шпаргалка для программиста
- Что такое Java Stream API
- Stream на примере простой задачи
- Преимущества Stream
- Как создавать стримы
- Методы стримов
- Конвейерные
- Терминальные
- Методы числовых стримов
- Еще несколько методов
- Решение задач с помощью Stream API
- Задачи про группу студентов
- Задачи на поиск в строке
- Заключение
Java 8 Stream API: шпаргалка для программиста
Обработка данных — стандартная задача при разработке. Раньше для этого приходилось использовать циклы или рекурсивные функции. С появлением в Java 8 Stream API процесс обработки данных значительно ускорился. Этот инструмент языка позволяет описать, как нужно обработать данные, кратко и емко.
Что такое Java Stream API
Это новый инструмент языка Java, который позволяет использовать функциональный стиль при работе с разными структурами данных.
Для начала стриму нужен источник, из которого он будет получать объекты. Чаще всего это коллекции, но не всегда. Например, можно взять в качестве источника генератор, у которого заданы правила создания объектов.
Данные в стриме обрабатываются на промежуточных операциях. Например: мы можем отфильтровать данные, пропустить несколько элементов, ограничить выборку, выполнить сортировку. Затем выполняется терминальная операция. Она поглощает данные и выдает результат.
Stream на примере простой задачи
Для наглядности посмотрим на примере использование стримов в сравнении со старым решением аналогичной задачи.
Задача — найти сумму нечетных чисел в коллекции.
Решение с методами стрима:
Здесь мы видим функциональный стиль. Без стримов эту же задачу приходится решать через использование цикла:
Да, на первый взгляд цикл выглядит более понятным. Но это вопрос опыта взаимодействия со стримами. Очень быстро привыкаешь к тому, что можно обрабатывать данные без использования циклов.
Преимущества Stream
Благодаря стримам больше не нужно писать стереотипный код каждый раз, когда приходится что-то делать с данными: сортировать, фильтровать, преобразовывать. Разработчики меньше думают о стандартной реализации и больше времени уделяют более сложным вещам.
Еще несколько преимуществ стримов:
- Поддержка слабой связанности. Чем меньше классы знают друг про друга, тем лучше.
- Распараллеливать проведений операций с коллекциями стало проще. Там, где раньше пришлось бы проходить циклом, стримы значительно сокращают количество кода.
- Методы Stream API не изменяют исходные коллекции, уменьшая количество побочных эффектов.
Даже сложные операции по обработке данных благодаря Stream API выглядят лаконично и понятно. В общем, писать становится удобнее, а читать — проще.
Как создавать стримы
В таблице ниже — основные способы создания стримов.
Источник | Способ | Пример |
Коллекция | collection.stream() | Collection collection = Arrays.asList(«f5», «b6», «z7»); Stream collectionS = collection.stream(); |
Значения | Stream.of(v1,… vN) | Stream valuesS = Stream.of(«f5», «b6», «z7»); |
Примитивы | IntStream.of(1, … N) | IntStream intS = IntStream.of(9, 8, 7); |
DoubleStream.of(1.1, … N) | DoubleStream doubleS = DoubleStream.of(2.4, 8.9); | |
Массив | Arrays.stream(arr) | String[] arr = <"f5","b6","z7">; Stream arrS = Arrays.stream(arr); |
Файл — каждая новая строка становится элементом | Files.lines(file_path) | Stream fromFileS = Files.lines(Paths.get(«doc.txt»)) |
Stream.builder | Stream.builder().add(. ). build() | Stream.builder().add(«f5»).add(«b6»).build() |
Есть и другие способы. Например, с перечисляемыми типами можно создавать стримы не только из существующих последовательностей, но еще и задавать range , причем сразу двух типов:
Стримы можно создавать не только из файлов, но и из списка объектов какой-либо директории или файлов, находящихся в какой-либо части дерева файловой системы.
Если требуется параллельный стрим, то просто напишите collection.parallelStream() .
Почти все перечисленные способы создания потоков не выглядят необычно для тех, кто привык постоянно работать с коллекциями. Но есть еще два интересных варианта: Stream.iterate и Stream.generate . Их предназначение — бесконечные стримы.
В Stream.iterate мы задаем начальное значение, а также указываем, как будем получать следующее, используя предыдущий результат:
Stream iterStream = Stream.iterate(1, m -> m + 1)
Stream.generate позволяет бесконечно генерировать постоянные и случайные значения, которые соответствуют указанному выражению.
Stream generateStream = Stream.generate(() -> «f5»)
Если хотите узнать больше об этих и других способах, читайте документацию Stream.
Методы стримов
В Java 8 Stream API доступны методы двух видов — конвейерные и терминальные. Кроме них можно выделить ряд спецметодов для работы с числовыми стримами и несколько методов для проверки параллельности/последовательности. Но это формальное разделение.
Конвейерных методов в стриме может быть много. Терминальный метод — только один. После его выполнения стрим завершается.
Пока вы не вызвали терминальный метод, ничего не происходит. Все потому, что конвейерные методы ленятся. Это значит, что они обрабатывают данные и ждут команды, чтобы передать их терминальному методу.
Конвейерные
Метод | Что сделает | Использование |
filter | отработает как фильтр, вернет значения, которые подходят под заданное условие | collection.stream().filter(«e22»::equals).count() |
sorted | отсортирует элементы в естественном порядке; можно использовать Comparator | collection.stream().sorted().collect(Collectors.toList()) |
limit | лимитирует вывод по тому, количеству, которое вы укажете | collection.stream().limit(10).collect(Collectors.toList()) |
skip | пропустит указанное вами количество элементов | collection.stream().skip(3).findFirst().orElse(«4») |
distinct | найдет и уберет элементы, которые повторяются; вернет элементы без повторов | collection.stream().distinct().collect(Collectors.toList()) |
peek | выполнить действие над каждым элементом элементов, вернет стрим с исходными элементами | collection.stream().map(String::toLowerCase).peek((e) -> System.out.print(«,» + e)). collect(Collectors.toList()) |
map | выполнит действия над каждым элементом; вернет элементы с результатами функций | Stream.of(«3», «4», «5») .map(Integer::parseInt) .map(x -> x + 10) .forEach(System.out::println); |
mapToInt , mapToDouble , |
mapToLong
Терминальные
Метод | Что сделает | Использование |
findFirst | вернет элемент, соответствующий условию, который стоит первым | collection.stream().findFirst().orElse(«10») |
findAny | вернет любой элемент, соответствующий условию | collection.stream().findAny().orElse(«10») |
collect | соберет результаты обработки в коллекции и не только | collection.stream().filter((s) -> s.contains(«10»)).collect(Collectors.toList()) |
count | посчитает и выведет, сколько элементов, соответствующих условию | collection.stream().filter(«f5»::equals).count() |
anyMatch | True , когда хоть один элемент соответствует условиям | collection.stream().anyMatch(«f5»::equals) |
noneMatch | True , когда ни один элемент не соответствует условиям | collection.stream().noneMatch(«b6»::equals) |
allMatch | True , когда все элементы соответствуют условиям | collection.stream().allMatch((s) -> s.contains(«8»)) |
min | найдет самый маленький элемент, используя переданный сравнитель | collection.stream().min(String::compareTo).get() |
max | найдет самый большой элемент, используя переданный сравнитель | collection.stream().max(String::compareTo).get() |
forEach | применит функцию ко всем элементам, но порядок выполнения гарантировать не может | set.stream().forEach((p) -> p.append(«_2»)); |
forEachOrdered | применит функцию ко всем элементам по очереди, порядок выполнения гарантировать может | list.stream().forEachOrdered((p) -> p.append(«_nv»)); |
toArray | приведет значения стрима к массиву | collection.stream().map(String::toLowerCase).toArray(String[]::new); |
reduce | преобразует все элементы в один объект | collection.stream().reduce((c1, c2) -> c1 + c2).orElse(0) |
Совет: подробнее изучите метод collect . Он позволяет гибко управлять преобразованием значений в разные типы: коллекции, массивы, map . Делается это благодаря статистическим методам Collectors .
Вот несколько интересных примеров:
- toList — стрим приводится к списку;
- toCollection — получаем коллекцию;
- toSet — получаем множество;
- toConcurrentMap , toMap — если нужен map ;
- summingInt , summingDouble , summingLong — если требуется получить сумму чисел;
- averagingInt , averagingDouble , averagingLong — если хотите вернуть среднее значение;
- groupingBy — если необходимо разбить коллекцию на части.
Это не все статистические методы Collectors . Другие возможности с подробным описанием смотрите в документации. Помимо тех Collectors , которые определены в документации, можно использовать собственноручно созданные, кастомные варианты.
Методы числовых стримов
Это специальные методы, которые работают только со стримами с числовыми примитивами.
Метод | Что сделает | Использование |
sum | вернет сумму чисел, представленных в коллекции | collection.stream().mapToInt((s) -> Integer.parseInt(s)).sum() |
average | вернет среднее арифметическое | collection.stream().mapToInt((s) -> Integer.parseInt(s)).average() |
mapToObj | преобразует числовой стрим в объектный | intStream.mapToObj((id) -> new Key(id)).toArray() |
Еще несколько методов
Напоследок посмотрим еще несколько полезных методов, которые помогают управлять последовательными и параллельными стримами — как минимум быстро их определять.
Метод | Что сделает | Использование |
isParallel | скажет, параллельный стрим или нет | someStream.isParallel() |
parallel | сделает стрим параллельным или вернет сам себя | someStream = stream.parallel() |
sequential | сделает стрим последовательным или вернет сам себя | someStream = stream.sequential() |
Стримы могут быть последовательными и параллельными. Первые выполняются в текущем потоке, вторые используют общий пул ForkJoinPool.commonPool() . В параллельном стриме элементы разделяются на группы. Их обработка проходит в каждом потоке по отдельности. Затем они снова объединяются, чтобы вывести результат. С помощью методов parallel и sequential можно явно указать, что нужно сделать параллельным, а что — последовательным.
Не рекомендуется применять параллельность для выполнения долгих операций (например, извлечения данных из базы), потому что все стримы работают с общим пулом. Долгие операции могут остановить работу всех параллельных стримов в Java Virtual Machine из-за того, что в пуле не останется доступных потоков.
Чтобы избежать такой проблемы, используйте параллельные стримы только для коротких операций, выполнение которых занимает миллисекунды, а не секунды и тем более минуты.
В Stream API по умолчанию скрыта работа с потоконебезопасными коллекциями, разделение на части и объединение элементов. Это отличное решение. Разработчику остается только выбирать нужные методы и следить за тем, чтобы не было зависимостей от внешних факторов.
Решение задач с помощью Stream API
Давайте изучим на практике, как работать с разными методами Stream API , на примере несложных задач.
Допустим, у нас есть коллекция состоящая из строк. Arrays.asList(«Highload», «High», «Load», «Highload») . Применим к ней разные методы.
Посчитаем, сколько раз объект « High » встречается в коллекции:
А теперь посмотрим, какой элемент в коллекции находится на первом месте. Если мы получили пустую коллекцию, то пусть возвращается 0 :
Благодаря методам filter и findFirst можно находить элементы, равные заданным в условии:
Допустим, нам нужно вернуть последний элемент. Получили пустую коллекцию — пусть возвращается 0 . Используем метод skip , чтобы пропустить заданное количество элементов. А findFirst , чтобы вывести первое встреченное совпадение:
collection.stream().skip(collection.size() — 1).findFirst().orElse(«0») // Highload
С помощью метода skip можно искать элементы по порядку. Например, пропустить первый и вывести второй:
Можно также использовать методы skip и limit , чтобы явно задавать, сколько элементов нужно пропустить, а сколько — вернуть. Полученные значения соберем в массив:
collection.stream().skip(1).limit(2).toArray()// [High, Load]
Аналогичным образом можно поиграться с методами min и max . Пусть у нас будет коллекция строк вида Arrays.asList(«f10», «f15», «f2», «f4») . Найти самый маленький элемент не составит труда:
С максимальным значением тоже все очень просто:
Посмотрим несколько примеров работы сортирующих методов. Используем ту же коллекцию строк, что и выше — Arrays.asList(«f10», «f15», «f2», «f4», «f4») . Единственное отличие — теперь в нем появился дубликат.
Первая задача — отсортировать строки в алфавитном порядке и добавить их в массив:
collection.stream().sorted().collect(Collectors.toList()) // [f2, f4, f4, f10, f15]
А вот чуть более интересное задание — нужно выполнить сортировку в обратном алфавитному порядке и удалить дубликаты. В массиве должны оказаться только уникальные значения:
Здесь мы используем не только sorted для сортировки, но и метод distinct для удаления неуникальных значений при обработке коллекции.
Задачи про группу студентов
Теперь давайте посмотрим чуть более комплексные, взрослые задачи. Например, у нас есть коллекция, которая имеет следующий вид:
Мы можем обрабатывать эти данные используя методы Stream API . Например, давайте найдем средний возраст студентов мужского пола. Естественно, это может быть не целочисленное значение.
Сначала создадим коллекцию студентов и опишем их:
Теперь мы можем использовать методы стримов для обработки этой коллекции. Посчитаем средний возраст, используя метод average :
Получилась немного странная группа студентов мужского пола, но средний возраст вполне себе студенческий. Что мы здесь сделали:
- Отфильтровали студентов по половому признаку.
- Выбрали для обработки их возраст.
- Вернули среднее арифметическое с помощью метода average.
Теперь давайте посмотрим, кому из наших студентов грозит получение повестки в этом году при условии, что призывной возраст установлен в диапазоне от 18 до 27 лет.
Не повезло Максиму. Он мужского пола, ему 20 лет. Другие студенты мужского пола не подходят под условие s.getAge() >= 18 && s.getAge() . Один младше 18 лет, другой — старше 27 лет. Единственная студентка в нашей выборке не подходит под условие s.getGender() == Gender.MAN . При этом по возрасту она вполне проходит по первым двум условиям. Но так как используется оператор && , в итоге мы получаем False .
Задачи на поиск в строке
Представим, что у нас есть большое количество логинов сотрудников. Нам нужно создать программу, которая будет выводить логины, начинающиеся на определенную букву. И использовать для решения этой задачи Stream API .
Вот как будет выглядеть код этой программы:
Программа предлагает ввести имена сотрудников. Все они сохраняются в массив ALL без предварительной обработки. Чтобы остановить ввод имен, нужно ввести пустую строку.
Сначала на экране выведется массив со всеми введенными именами. Чтобы отфильтровать их, нужно добавить условие. В нашем случае это будет первая буква — например, ‘ a ‘.
Для фильтрации используется метод filter . Затем данные записываются в результирующий стрим. Чтобы вывести количество имен подходящих под заданное ранее условие, мы используем метод count . Вот такое простое и элегантное решение.
Заключение
Stream в Java дает разработчикам удобные инструменты для обработки данных в коллекциях. Методы позволяют проще обрабатывать объекты и писать меньше кода.
Но стрим — не серебряная пуля. Опытные разработчики собрали несколько советов по их использованию:
- Стримы можно не использовать, если задача решается красиво и эффективно без них.
- Не обязательно сохранять stream в переменную. Достаточно использовать цепочку вызовов методов.
- Старайтесь ограничить или почистить стрим от лишних элементов, прежде чем выполнять преобразования.
- Используйте параллельные потоки разумно. В отдельных случаях разбиение, обработка в разных потоках и последующее объединение данных могут быть дороже, чем работа в одном потоке.
Чтобы закрепить свои знания, посмотрите это наглядное пособие по Java Stream API . В нем рассматривается функциональный подход к работе с коллекциями. В видео есть примеры создания стримов из объектов файловой системы, примитивов, объектов, а также примеры использования конвейерных и терминальных методов:
Highload нужны авторы технических текстов. Вы наш человек, если разбираетесь в разработке, знаете языки программирования и умеете просто писать о сложном!
Откликнуться на вакансию можно здесь .
Источник