Что нужно знать о Java Stream API |
||
МЕНЮ Главная страница Поиск Регистрация на сайте Помощь проекту Архив новостей ТЕМЫ Новости ИИ Голосовой помощник Разработка ИИГородские сумасшедшие ИИ в медицине ИИ проекты Искусственные нейросети Искусственный интеллект Слежка за людьми Угроза ИИ ИИ теория Внедрение ИИКомпьютерные науки Машинное обуч. (Ошибки) Машинное обучение Машинный перевод Нейронные сети начинающим Психология ИИ Реализация ИИ Реализация нейросетей Создание беспилотных авто Трезво про ИИ Философия ИИ Big data Работа разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика
Генетические алгоритмы Капсульные нейросети Основы нейронных сетей Распознавание лиц Распознавание образов Распознавание речи Творчество ИИ Техническое зрение Чат-боты Авторизация |
2022-05-17 14:09 Всем привет! В этой статье я хочу познакомить вас, на мой взгляд, с одним из самых значительных нововведений в Java со времен ее появления — это Java Stream API. Что такое Java Stream API? Зачем? И какие дает преимущества? Очень часто, когда мы пишем программу, нам нужно обрабатывать наши данные. Для обработки данных мы используем циклы либо рекурсивные функции для обхода наших данных. Java Stream API был создан для того, чтобы помочь пользователям ускорить и упростить обработку данных. Сам по себе API предоставляет инструмент, который позволяет нам дать рецепт того как обрабатывать объекты. Если проводить параллели с реальным миром, то давайте представим, что у нас есть некий завод по производству мебели под заказ. Грузовые автомобили привозят бревна на завод. На данном заводе у нас есть люди которых мы обучили что-то делать с древесиной, чтобы из нее получилась мебель: они просматривают каждое бревно на предмет дефектов и отфильтровывают брак, распиливают бревна, обрабатывают доски, собирают при помощи гвоздей и клея и защищают готовую продукцию при помощи лака. Последний элемент в этой цепи — покупатель, который приходит на завод и делает заказ. Без покупателя нет смысла запускать все производство, поэтому весь процесс стартует во время запуска производства. В мире Java такой завод называется Stream API. Этот API представляет собой библиотеку, которая помогает в функциональном стиле кратко, но емко описывать, как обработать данные. Как и в примере про завод, у каждого стрима должен быть источник объектов. Этим источником информации чаще всего бывает коллекция, так как именно в них мы и храним наши данные, но это не обязательно — может быть и какой-то генератор, который генерирует объекты по заданному правилу, примеры мы рассмотрим позже. В Java Stream API также предусмотрены промежуточные операции. Они выполняют роль рабочих. Операции описывают процесс обработки объектов. В конце каждого стрима должна быть терминальная операция, которая должна поглотить все обработанные данные. В примере про завод мы видели, что заказчик становится триггером начала производства и является последним звеном в работе завода — он забирает всю продукцию. Рассмотрим простейший стрим. Создадим класс бревно и поместим несколько бревен в коллекцию: class Log{ String type; int count; // конструктор и гетеры опущены } List<Log> logs = List.of( new Log("Сибирская сосна", 10), new Log("Дуб монгольский", 30), new Log("Берёза карликовая", 5)); У коллекций есть метод Stream<Log> stream = logs.stream(); Получив ссылку на стрим, мы можем начать обрабатывать поток наших данных. Отфильтруем бревна, количество которых меньше 7 и оставим только те, которые не являются дубом. Выглядеть это будет так: Stream<Log> filteredStream = stream.filter(x -> x.getCount() > 7) .filter(x -> !"Дуб монгольский".equalsIgnoreCase(x.getType())); Мы добавили фильтры и получили стрим, в котором описан процесс обработки всех наших бревен. Теперь мы должны добавить к нему терминальную операцию, чтобы запустить поток данных из коллекции: filteredStream.forEach(x -> System.out.println(x.getType())); В этом примере конечная операция принимает оставшиеся элементы после фильтрации и распечатывает их. Стоит особо упомянуть, что второй раз вызвать терминальную операцию не получится — стрим является «одноразовым» объектом. Это сделано авторами библиотеки для того, чтобы можно было корректно обрабатывать данные, которые имеют ограниченное время жизни. Например, если обрабатывать пакеты из интернета, то данные в стрим могут попасть только один раз, поэтому повторный вызов теряет всякий смысл. Как упоминалось ранее, создать источник данных можно разными способами. Рассмотрим самые популярные. Способы создания источника данных В начале пройдемся по методам объявленным в интерфейсе Stream.
Пример: Stream<String> stringStream = Stream.of("asd", "aaa", "bbb"); Для создания пустого стрима существует метод:
Патерн строитель поддерживается библиотекой, потому получив объект строителя Если у нас есть два стрима, мы можем объеденить их в один вызвав метод:
Пример: Stream.concat(Stream.of("aaa", "bbb", "ccc"), Stream.of("111", "222", "333")) В итоге мы получим стрим, в котором будет находится шесть элементов. Стрим не обязательно должен поглощать какие-то данные, можно создать генератор, который будет поставлять в наш стрим с помощью метода Stream.generate(() -> Math.random()).forEach(System.out::println); Так как генератор может бесконечно генерировать стрим и в примере выше мы получим бесконечный вывод на экран случайных значений, необходимо добавить промежуточную операцию Аналогичную функциональность предоставляет класс Random. В нем уже есть методы которые создают стримы из случайных чисел. new Random().ints() new Random().doubles() new Random().longs() Тут стоить отметить, что порой, когда стрим состоит из одних чисел, использование оберток над примитивными типами будет сильно влиять на производительность. Поэтому создатели стримов добавили специальные типы стримов для примитивных типов: LongStream() DoubleStream() IntStream() Это такие же стримы, но как понятно из названия оперируют они только одним типом данных. Также получить стрим из примитивов можно воспользовавшись методами утилитного класса Теперь мы перейдем к самому интересному — в интерфейсе Collection добавлен дефолтный метод, который возвращает нам стрим. То есть любая коллекция дает нам возможность превратить ее в стрим: List.of("a", "b", "c").stream().forEach(System.out::println); Просто вызвав метод у коллекции мы получили стрим. Это самый частый способ получить стрим из набора данных. Познакомившись с основными методами создания теперь мы можем перейти к промежуточным операциям. Именно они позволят нам обработать наш поток данных. Промежуточные операции Мы ранее уже знакомились с операцией фильтр, она позволяет нам написать выражение, которое будет проверятся для каждого элемента и если выражение истинно, то элемент может проходить дальше. Но на нашем заводе мы делаем намного больше чем просто фильтруем бревна. Для того, чтобы дерево превратилось в мебель его нужно преобразовать. Для этого пригодится самая популярная функция — Возьмем наш пример выше и попробуем преобразовать List<Log> logs = List.of( new Log("Сибирская сосна", 10), new Log("Дуб монгольский", 30), new Log("Берёза карликовая", 5)); logs.stream().map(x -> x.getType()).forEach(System.out::println); Функция map принимает реализацию функционального интерфейса На вход мы получаем объект типа T, а возвращаем объект типа R. То есть наш стрим, который был типизирован объектом Log становится типизирован объектом, который возвращает Промежуточные операции можно конкатенировать между собой, то есть мы можем добавить еще несколько преобразований: logs.stream().map(Log::getType).map(x -> x.split(" ")) .forEach(System.out::println); Во втором преобразовании мы разбили каждую строку на массив строк. Но если мы запустим приложение, мы увидим, что на экран не вывелись строки, а вывелось logs.stream().map(Log::getType).map(x -> x.split(" ")).flatMap(x -> Arrays.stream(x)).forEach(System.out::println); На вход flatMap() поступает функция, задача которой получить из объекта стрим и конкатенировать его с другими. Таким образом, мы создаем стримы из массивов строк и соединяем их вместе. Попробуем теперь получить список всех букв, которые встречаются в нашем стриме. Для этого воспользуемся методом logs.stream().map(Log::getType) .map(x -> x.split(" ")).flatMap(Arrays::stream) .map(String::chars).forEach(System.out::println); Но запустив пример выше мы получили стрим стримов — Но так с ним работать не удобно, а обычный flatMap не сработает, поэтому для примитивных стримов существуют специальные операции для их преобразований: IntStream chars = logs.stream().map(Log::getType).map(x -> x.split(" ")).flatMap(Arrays::stream).map(String::chars).flatMapToInt(x -> x); chars.forEach(x1 -> System.out.println((char)x1)); Значение функции выше В итоге предыдущий пример вывел нам на экран побуквенно каждое название типа дерева. Что делать, если мы хотим вывести на экран только по одной букве, убрав повторяющиеся буквы? Для этого мы можем воспользоваться промежуточной операцией Для того чтобы отсортировать буквы воспользуемся операцией IntStream chars = logs.stream().map(Log::getType).map(x -> x.split(" ")).flatMap(Arrays::stream). map(String::chars).flatMapToInt(x -> x).distinct().sorted(); Стоит отметить, что операция Для того, чтобы ограничить бесконечные операции существует операция IntStream chars = logs.stream().map(Log::getType).map(x -> x.split(" ")).flatMap(Arrays::stream). map(String::chars).flatMapToInt(x -> x).distinct().limit(3).sorted() В примере выше мы с помощью функции limit ограничили наш стрим до трех элементов, которые уже позже попали в Противоположная Порой это не особо удобно, а порой невозможно указать заранее сколько элементов пропустить или поглотить. Поэтому в стримы были добавлены дополнительные промежуточные операции, которые принимают предикат: Простой пример приведен ниже: new Random().ints().takeWhile(x -> x % 7 != 0).forEach(System.out::print); Стрим будет генерировать новые значения, пока остаток от деления на 7 сгенерированного значения не будет равен 0. И последняя операция, которую хотелось бы описать — это boxed. Ее стоит применять в том случае, если мы хотим превратить наш стрим примитивов в объектный стрим. То есть в примере выше, если добавить ее, то наш стрим перестанет быть IntStream, а станет Есть еще несколько промежуточных операций, но я расскажу только об одной, на мой взгляд, самой важной. Это операция Терминальные операции После знакомства с основными промежуточными операциями мы плавно подошли к заключению. Осталось рассмотреть терминальные операции. Это операции, которые как бы «запускают» наш стрим. Мы можем создать стрим и добавить в него любое количество промежуточных операций, но они не будут выполнены пока не будут добавлена терминальная операция. Выше мы уже применяли одну из самых популярных операций — Кроме этого терминальная операция может и возвращать значение. Рассмотрим самые распространенные — Функции Функции Теперь стоит перейти к более сложным функциям. Часто в качестве результата стрима мы хотим получить набор из новых объектов, которые были созданы в результате обработки. Для этого удобно поместить их в массив или коллекцию. В Java Stream API было добавлено несколько методов, которые дают соответствующую функциональность. Вызвав терминальную операцию Следующая операция, которую стоить упомянуть Для того, чтобы получить сумму первых 100 членов стрима из произвольных значений, запишем: new Random().ints(100).reduce(0, (x, y) -> x + y); Мы передаем начальный элемент для сложения, в нашем случае он 0 и бинарную функцию, которая описывает как объединить два значения из стрима. Если же мы хотим перенести этот набора чисел в коллекцию, то для этого нам надо будет указать как создать коллекцию и как в нее помещать элементы: List<Integer> ints = new Random().ints(100) .boxed() // оборачиваем, так как коллекции не работают с примитивами .reduce(new ArrayList<>(), (x, y) -> { x.add(y); return x; }, (a, b) -> { a.addAll(b); return a }); В функции reduce мы передали наш начальный аргумент — новую пустую коллекцию. Потом описали правило, по которому будем объединять коллекцию и элементы стрима. И в конце описали как мы будем объединять две коллекции. Чтобы сократить подобную запись, была создана терминальная операция List<Integer> ints = new Random().ints(100) .boxed() .collect(Collectors.toList()); То есть вся логика комбинирования элементов хранится в структуре данных под названием коллектор. Создатели Java Stream API добавили в библиотеку большое количество коллекторов, рассмотрим их. Выше мы уже познакомились с коллектором, который комбинирует элементы в список. Если нам нужно собрать элементы в коллекцию типа Set, то достаточно просто использовать коллектор Существует более общий метод Более сложный коллектор — Подсчитаем количество букв, которые мы получили в стриме chars.collect(Collectors.toMap(x -> x, x -> 1, Integer::sum)) Для ключа мы используем саму букву без изменений Операция chars.collect(Collectors.partitioningBy(Character::isLowerCase)) Коллекторы могут быть скомбинированы друг с другом, что дает большую гибкость. В примере выше мы видим, что некоторые буквы повторяются, мы этого не хотим поэтому добавим еще один коллектор, который соберет все в Set: chars.collect(Collectors.partitioningBy(Character::isLowerCase, Collectors.toSet())) Метод Чтобы самостоятельно реализовать коллектор можно воспользоваться статическим методом: Collector<T, R, R> of(Supplier<R> supplier, BiConsumer<R, T> accumulator, BinaryOperator<R> combiner, Characteristics... characteristics) Очень похоже на метод В этой короткой статье мы познакомились с, на мой взгляд, самой крутой штукой в языке Java с момента ее создания. Стримы позволяют существенно упростить, а соответственно ускорить разработку кода. Возможность практически бесплатно сделать стрим параллельным, тем самым повысив производительность кода в разы, делает стримы инструментом номер одни в руках каждого разработчика. Удачного кодинга! ) Источник: m.vk.com Комментарии: |
|