Полное руководство по java stream api

Java

Основная тема этой статьи-расширенные темы обработки данных с использованием новой функциональности, добавленной в Java 8, — Stream API и Collector API. Чтобы получить максимальную отдачу от этой статьи, вы уже должны быть знакомы с основными API Java, классами Object и String, а также API Collection.

StreamAPI

Пакет java. util. streamсостоит из классов, интерфейсов и множества типов, позволяющих выполнять операции функционального стиля над элементами. Java 8 вводит понятие потока, который позволяет программисту обрабатывать данные описательно и полагаться на многоядерную архитектуру без необходимости писать какой-либо специальный код.

Что такое Stream?

Stream представляет собой последовательность объектов, полученных из источника, над которыми могут выполняться агрегатные операции.

С чисто технической точки зрения поток-это типизированный интерфейс-поток T. Это означает, что поток может быть определен для любого вида объекта, потока чисел, потока символов, потока людей или даже потока города.

С точки зрения разработчика, это новая концепция, которая может просто выглядеть как коллекция, но на самом деле она сильно отличается от коллекции. Есть несколько ключевых определений, которые нам нужно пройти, чтобы понять это понятие потока и почему оно отличается от коллекции.

Поток не содержит никаких данных

Самое распространенное заблуждение, к которому я хотел бы обратиться в первую очередь, — поток не содержит никаких данных. Это очень важно иметь в виду и понимать.

В потоке нет данных, однако есть данные, хранящиеся в коллекции

Collection — это структура, в которой хранятся ее данные. Поток существует только для того, чтобы обрабатывать данные и извлекать их из данного источника или перемещать в пункт назначения. Источником может быть коллекция, хотя это также может быть массив или ресурс ввода-вывода. Поток будет соединяться с источником, потреблять данные и каким-то образом обрабатывать элементы в нем.

Поток не должен изменять источник

Поток не должен изменять источник данных, которые он обрабатывает. На самом деле это не навязывается компилятором самой JVM, поэтому это просто контракт. Если я должен построить свою собственную реализацию потока, я не должен изменять источник данных, которые я обрабатываю. Хотя это совершенно нормально-изменять данные в потоке.

Почему это так? Потому что если мы хотим обрабатывать эти данные параллельно, мы собираемся распределить их между всеми ядрами наших процессоров, и мы не хотим иметь никаких проблем с видимостью или синхронизацией, которые могли бы привести к плохой производительности или ошибкам. Избегание такого рода помех означает, что мы не должны изменять источник данных во время их обработки.

Источник может быть неограниченным

Это означает, что поток сам по себе может обрабатывать столько данных, сколько мы хотим. Неограниченность не означает, что источник должен быть бесконечным. На самом деле, источник может быть конечным, но мы можем не иметь доступа к элементам, содержащимся в этом источнике.

Предположим, что источником является простой текстовый файл. Текстовый файл имеет известный размер, даже если он очень большой. Также предположим, что элементы этого источника на самом деле являются строками этого текстового файла.

Теперь мы можем знать точный размер этого текстового файла, но если мы не откроем его и не пройдем вручную по содержимому, мы никогда не узнаем, сколько в нем строк. Это то, что означает unbounded — мы не всегда можем заранее знать количество элементов, которые поток будет обрабатывать из источника.

Таковы три определения потока. Таким образом, из этих трех определений мы можем видеть, что stream на самом деле не имеет ничего общего с collection. Collection содержит свои данные.Collection может изменять данные, которые она содержит. И конечно, коллекция содержит известное и конечное количество данных.

Краткое описание терминальных методов работы со стримами

Терминальная операция в Java — это метод, применяемый к потоку в качестве заключительного шага. Дополнительные потоковые операции не разрешены, поскольку терминальная операция никогда не создает потоковый объект. Типичным примером терминальной операции является метод forEach, который часто используется для печати элементов объекта stream. Другим полезным примером является метод reduce, который производит один результат (например, сумму) из потока. Другие хорошие примеры включают min и max.

Статический блок выполняется один раз, когда программа загружается в память. Программа отобразит список целых чисел, а затем применит несколько терминальных операций. Первая терминальная операция-это forEach, и эта операция будет печатать каждый элемент потока.

Далее вызывается операция min. Этот метод возвращает минимальное значение, хранящееся в потоке как необязательное. Optional-это новый класс в Java 8, который предоставляет метод get для возврата значения переменной, если оно не равно null.

Следующая операция-макс. Этот метод возвращает максимальное значение, хранящееся в потоке. Наконец, вызывается метод reduce. Параметр, передаваемый этому методу, является лямбда-выражением. В этом случае выражение добавляет один элемент к следующему элементу, накапливая итог, который возвращается как необязательный.

Создание потока

Мы можем генерировать поток с помощью нескольких методов:

  • stream(). Метод stream () возвращает последовательный поток с коллекцией в качестве источника. В качестве источника можно использовать любую коллекцию объектов:

privateList<String>list = newArrays.asList(«Scott», «David», «Josh»);
list.stream();

  • parallelStream(). Метод parallelStream() возвращает параллельный поток с коллекцией в качестве источника:

privateList<String>list = newArrays.asList(«Scott», «David», «Josh»);
list.parallelStream().forEach(element ->method(element));

Проблема с параллельными потоками заключается в том, что при выполнении такой операции среда выполнения Java разделяет поток на несколько подпотоков. Он выполняет агрегатные операции и объединяет результат.

В нашем случае он вызывает метод с каждым элементом в потоке параллельно.
Хотя это может быть палка о двух концах, поскольку выполнение тяжелых операций таким образом может блокировать другие параллельные потоки, поскольку он блокирует потоки в пуле.

  • Stream.of(). Метод staticof() можно использовать для создания потока из массива объектов или отдельных объектов:

Stream.of(newEmployee(«Gosha»), newEmployee(«Vlad»), newEmployee(«Egor»));

  • Stream.builder(). И, наконец, вы можете использовать метод static .builder() для создания потока объектов:

Stream.builder<String>streamBuilder = Stream.builder();
streamBuilder.accept(«Gosha»);
streamBuilder.accept(«Vlad»);
streamBuilder.accept(«Egor»);
Stream<String>stream = streamBuilder.build();

Вызывая метод. build (), мы упаковываем принятые объекты в обычный поток.

Фильтрация с помощью потока

public class FilterExample {
public staticvoidmain(String[] args) {
List<String> fruits = Arrays.asList(«Apple», «Banana», «Cherry», «Orange»);
// Traditional approach
for(String fruit : fruits) {
if (!fruit.equals(«Orange»)) {
System.out.println(fruit + » «);
}
}

// Stream approach
fruits.stream()
.filter(fruit -> !fruit.equals(«Orange»))
.forEach(fruit ->System.out.println(fruit));
}
}

Традиционный подход к фильтрации одного плода был бы классическим для каждого цикла. Второй подход использует поток для фильтрации элементов потока, соответствующих данному предикату, в Новый Поток, возвращаемый методом.

Кроме того, этот подход использует метод forEach (), который выполняет действие для каждого элемента возвращаемого потока. Вы можете заменить это чем-то, что называется ссылкой на метод. В Java 8 ссылка на метод — это сокращенный синтаксис для лямбда-выражения, которое выполняет только один метод.

Метод ведения синтаксис очень прост, и вы можете даже заменить предыдущее:

lambdaexpression .filter(fruit -> !fruit.equals(«Orange»)) withit:
Object::method;

Давайте обновим пример и используем ссылки на методы и посмотрим, как это выглядит:

publicclassFilterExample{
public static voidmain(String[] args) {
List<String> fruits = Arrays.asList(«Apple», «Banana», «Cherry», «Orange»);

fruits.stream()
.filter(FilterExample::isNotOrange)
.forEach(System.out::println);
}
private static booleanisNotOrange(String fruit) {
return!fruit.equals(«Orange»);
}
}

Потоки проще и лучше использовать с лямбда-выражениями, и этот пример показывает, насколько простым и чистым выглядит синтаксис по сравнению с традиционным подходом.

Сопоставление со Stream

Традиционный подход состоял бы в том, чтобы перебирать список с помощью расширенного цикла for:

List<String>models = Arrays.asList(«BMW», «Audi», «Peugeot», «Fiat»);
System.out.print(«Imperative style: » + «\n»);
for(String car : models) {
if(!car.equals(«Fiat»)){
Car model = newCar(car);
System.out.println(model);
}
}

С другой стороны, более современный подход заключается в использовании потока для отображения:

List<String>models = Arrays.asList(«Volkswagen», «Mercedes», «Lada», «Mazda»);
System.out.print(«Functional style: » + «\n»);
models.stream()
.filter(model -> !model.equals(«Lada»))
// .map(Car::new) // Method reference approach
// .map(model -> new Car(model)) // Lambda approach
.forEach(System.out::println);

Чтобы проиллюстрировать отображение, рассмотрим этот класс:

private String name;
publicCar(String model) {
this.name= model;
}
// getters and setters
@Override
publicString toString() {
return»name='» + name + «‘»;
}

Важно отметить, что список models– это список строк, а не список автомобилей. Метод. map() ожидает объект типа T и возвращает объект типа R. По сути, мы превращаем String в тип автомобиля.

Если вы запускаете этот код, императивный стиль и функциональный стиль должны возвращать одно и то же.

Коллекционирование со Stream

Иногда вы хотите преобразовать поток в коллекцию или карту. Использование коллекторов служебных классов и предлагаемых ими функций:

List<String>models = Arrays.asList(«Volkswagen», «Mercedes», «Lada», «Mazda»);
List<Car>carList = models.stream()
.filter(model -> !model.equals(«Fiat»))
.map(Car::new)
.collect(Collectors.toList());

Совпадение с потоком

Классическая задача состоит в том, чтобы классифицировать объекты по определенным критериям. Мы можем сделать это, сопоставив необходимую информацию с информацией об объекте и проверив, действительно ли это то, что нам нужно:

List<Car>models = Arrays.asList(newCar(«Volkswagen»2013), newCar(«Mercedes», 2017), newCar(«Lada», 2014));
boolean all = models.stream().allMatch(model ->model.getYear() >2010);
System.out.println(«Are all of the models newer than 2012?: » + all);
boolean any = models.stream().anyMatch(model ->model.getYear() >2016);
System.out.println(«Are there any models newer than 2016: » + any);
boolean none = models.stream().noneMatch(model ->model.getYear() <2010);
System.out.println(«Is there a car older than 2010: » + none);

  1. allMatch () — возвращает true, если все элементы этого потока соответствуют предоставленному предикату.
  2. anyMatch () — возвращает true, если какой-либо элемент этого потока соответствует предоставленному предикату.
  3. noneMatch () — возвращает true, если ни один элемент этого потока не соответствует предоставленному предикату.

В предыдущем примере кода все заданные предикаты удовлетворены, и все они вернут true.

Почему порядок выполнения в streamимеет значение

Прелесть Javastreams заключается в возможности объединения нескольких операций в «конвейер». Он может заменить большинство циклов for в вашем коде, особенно те, которые просто перемещают данные из одной структуры данных в другую (например, из List<YourObject> в Map<String, YourObject>. Но вы должны помнить одну вещь: каждый шаг в потоке будет вызываться до тех пор, пока элемент не будет отклонен.

Фильтруя сначала, мы собираемся ограничить операции map/sorted до минимума: filter 5 раз, map 2 раза, sort 1 раз и forEach 2 раза, что в общей сложности экономит нам 10 операций. В этом примере это может показаться не таким уж большим делом, но обычно мы имеем дело с более чем 5 пунктами, и операция с картой может быть дорогостоящей, поэтому делать меньше всегда лучше.

При необходимости подготовьте данные для более легкой фильтрации. Сначала отфильтруйте, если это возможно. Меньшее количество элементов равно меньшему количеству операций на этом пути. Если нет возможности сначала отфильтровать, попробуйте сначала использовать более дешевые операции, отфильтровать, а затем более дорогие.

Недостатки JavaAPIStream

Параллельные потоки действительно могут замедлить вас

Java 8 обещает параллелизм как одну из самых ожидаемых новых функций. Метод. parallelStream () реализует это в коллекциях и потоках. Он разбивает их на подзадачи, которые затем запускаются в отдельных потоках для обработки, они могут идти в разные ядра, а затем объединяться, когда они закончат. Все это происходит под капотом с помощью фреймворка fork/join. Хорошо, звучит круто, это должно ускорить работу с большими наборами данных в многоядерных средах, не так ли?

Нет, это действительно может заставить ваш код работать медленнее, если он не используется правильно. Примерно на 15% медленнее на этом бенчмарке мы бежали, но могло быть и хуже. Допустим, мы уже запускаем несколько потоков и используем .parallelStream() в некоторых из них, добавляя все больше и больше потоков в пул. Это может легко превратиться в нечто большее, чем наши ядра могут справиться, и замедлить все из-за увеличения переключения контекста.

Для чего Jigsaw?

Цель головоломки заключается в том, чтобы сделать Java модульные и сломать JRE, чтобы интероперабельных компонентов. Мотивация, стоящая за этим, в первую очередь исходит из желания иметь лучшую, более быструю и сильную встроенную Java. Я стараюсь избегать упоминания «Интернета вещей», но там я это сказал. Уменьшение размеров банок, повышение производительности и повышение безопасности-вот еще некоторые из обещаний этого амбициозного проекта.

Так где же он? Jigsaw совсем недавно вступила в фазу 2, прошла исследовательскую фазу и теперь переключается на качественный дизайн и внедрение производства, говорит Марк Рейнхольд, главный архитектор Oracle по Java. Проект сначала планировалось завершить в Java 8 и было отложено до Java 9, ожидалось, что это будет одна из его флагманских новых функций.

Проблемы, которые все еще существуют

Проверенные Исключения. Никто не любит шаблонный код, и это одна из причин, почему лямбды стали такими популярными. Думая о шаблонных исключениях, независимо от того, нужно ли вам логически ловить или иметь какое-то отношение к проверенному исключению, вам все равно нужно его ловить. Даже если это то, что никогда не произойдет, как это исключение, которое никогда не сработает:

Функциональное программирование

Функциональное программирование было возможно с помощью Java и раньше, хотя это довольно неудобно. Java 8 улучшает это, в частности, с помощью лямбд. Это очень приветствуется, но не такой огромный сдвиг, который был изображен ранее. Определенно более элегантный, чем в Java 7, но некоторые изгибы назад все еще необходимы, чтобы быть действительно функциональным.

Один из самых яростных обзоров по этому вопросу исходит от Пьера-Ивасомона, где в серии постов он внимательно рассматривает различия между парадигмами функционального программирования и способом их реализации в Java.

Итак, Java или Scala? Принятие более функциональных современных парадигм в Java является знаком одобрения для Scala, которая уже некоторое время играет с лямбдами. Лямбды действительно производят много шума, но есть гораздо больше функций, таких как черты, ленивая оценка и неизменяемые объекты, которые имеют большое значение.

Методы по умолчанию отвлекают

Методы по умолчанию позволяют реализовать функцию по умолчанию в самом интерфейсе. Это, безусловно, одна из самых крутых новых функций Java 8, но она несколько мешает тому, как мы привыкли делать вещи. Так почему же все-таки это было введено? А что с ним не делать?

Основная мотивация методов по умолчанию заключалась в том, что если в какой-то момент нам нужно добавить метод в существующий интерфейс, мы можем сделать это без переписывания реализации. Делает его совместимым со старыми версиями.

Функциональные интерфейсы в потоках Java 8

Некоторые из наиболее часто используемых функциональных интерфейсов в методах Java 8 Stream API:
Функция и Бифункция: функция представляет собой функцию, которая принимает один тип аргумента и возвращает другой тип аргумента.

Function<T, R> — это обобщенная форма, где T — Тип входных данных функции, а R-тип результата функции.

Для обработки примитивных типов существуют специальные интерфейсы функций — ToIntFunction, ToLongFunction, ToDoubleFunction, ToIntBiFunction, ToLongBiFunction, ToDoubleBiFunction, LongToIntFunction, LongToDoubleFunction, IntToLongFunction, IntToDoubleFunction и т. д.

JavaStream: промежуточные и терминальные операции

ОперацииJavaStream API, возвращающие новый поток, называются промежуточными операциями. В большинстве случаев эти операции носят ленивый характер, поэтому они начинают производить новые элементы потока и отправляют его на следующую операцию. Промежуточные операции никогда не являются операциями, производящими конечный результат. Обычно используемые промежуточные операции-filter и map.

Java 8 Stream API- это операции, которые возвращают результат или производят побочный эффект. Как только терминальный метод вызывается в потоке, он потребляет поток, и после этого мы не можем его использовать.

Терминальные операции нетерпеливы по своей природе, то есть они обрабатывают все элементы в потоке, прежде чем вернуть результат. Обычно используются терминальные методы forEach, toArray, min, max, findFirst, anyMatch, allMatch и т. д. Вы можете идентифицировать терминальные методы по типу возврата, они никогда не вернут поток.

Операции короткого замыкания потока Java

Промежуточная операция называется коротким замыканием, если она может произвести конечный поток для бесконечного потока. Например, limit() и skip () — это две промежуточные операции короткого замыкания.
Терминальная операция называется коротким замыканием, если она может завершиться за конечное время для бесконечного потока. Например, anyMatch,allMatch, noneMatch, findFirst и findAny являются терминальными операциями короткого замыкания.

Что нового в Java 13

  • JEP 350 Dynamic CDS Archives

Java 10 представила класс приложений JEP 310-обмен данными. Этот JEP упрощает процесс создания архивов компакт-дисков. Эта команда создает файл архива компакт-дисков из.jar.

$ java -XX:ArchiveClassesAtExit=hello.jsa -cp hello.jar Hello

Эта команда запускает файл. jar с существующим архивом компакт-дисков.

$ bin/java -XX:SharedArchiveFile=hello.jsa -cphello.jarHello

Совместное использование данных классов (CDS) повышает производительность запуска, создавая архив данных классов один раз и повторно используя его, чтобы JVM не нужно было воссоздавать его снова.

  • JEP 351 ZGC: незафиксированная неиспользуемая память

Java 11 представила сборщик мусора JEP 333: Z (экспериментальный); он обеспечивает короткое время паузы при очистке памяти кучи. Однако он не возвращал неиспользуемую память кучи в операционную систему, даже если она была неиспользуемой в течение длительного времени.

  • JEP-353 переопределяет устаревший API сокетов

Основные реализации java. net. Socketи java.net.ServerSocket-это древний, восходящий к JDK 1.0, смесь устаревшего кода Java и C, который трудно поддерживать и отлаживать. Этот JEP вводит новые базовые реализации для API сокетов, которые являются реализацией по умолчанию в Java 13.

До Java 13 он использовал PlainSocketImpl для SocketImpl. Этот JEP усиливается ZGC на возврат неиспользованных динамической памяти операционной системы.

ServerSocket.java
public class ServerSocketimplementsjava.io.Closeable{
/**
* The implementation of this Socket.
*/
privateSocketImplimpl;
}

Java 13 представила новый класс NioSocketImpl в качестве заменыPlainSocketImpl. Однако, если что-то пойдет не так, мы все равно можем вернуться к старой реализации PlainSocketImpl, установив системное свойство jdk.net.usePlainSocketImpl.

Рассмотрим простой пример сокета:

importjava.io.IOException;
importjava.net.ServerSocket;
importjava.net.Socket;
public class JEP353 {
public static void main(String[] args) {
try(ServerSocketserverSocket = newServerSocket(8888)){
boolean running = true;
while(running){
Socket clientSocket = serverSocket.accept();
//do something with clientSocket
}

} catch (IOException e) {
e.printStackTrace();
}
}
}

Вывод

Большинство людей сегодня используют Java 8. Хотя не все используют потоки. Просто потому, что они представляют собой новый подход к программированию и представляют собой прикосновение к программированию функционального стиля наряду с лямбда-выражениями для Java, не обязательно означает, что это лучший подход. Они просто предлагают новый способ делать вещи.

Разработчики сами решают, следует ли полагаться на функциональное или императивное Программирование. При достаточном уровне упражнений сочетание обоих принципов может помочь вам улучшить ваше программное обеспечение.

Оцените статью
Образовательный портал WELCOME4U.RU
Добавить комментарий

Adblock
detector