Java 8 Поток – Поток Java

Добро пожаловать на учебник по Java 8 Stream API. В последних нескольких постах о Java 8 мы рассмотрели Изменения в интерфейсе Java 8 и Функциональные интерфейсы и лямбда-выражения. Сегодня мы рассмотрим один из основных API, введенных в Java 8 – Java Stream.

Java 8 Stream

  1. Java 8 Stream
  2. Коллекции и Java Stream
  3. Функциональные интерфейсы в Java 8 Stream
    1. Функция и BiFunction
    2. Предикат и BiPredicate
    3. Потребитель и BiConsumer
    4. Поставщик
  4. java.util.Optional
  5. java.util.Spliterator
  6. Промежуточные и терминальные операции Java Stream
  7. Операции Java Stream с коротким замыканием
  8. Примеры Java Stream
    1. Создание потоков Java
    2. Преобразование потока Java в коллекцию или массив
    3. Промежуточные операции Java Stream
    4. Терминальные операции Java Stream
  9. Ограничения Java 8 Stream API

Поток Java

Прежде чем мы рассмотрим примеры использования Java Stream API, давайте посмотрим, зачем он нужен. Предположим, мы хотим перебрать список целых чисел и найти сумму всех чисел, больших 10. До Java 8 подход к этому был бы следующим:

private static int sumIterator(List<Integer> list) {
	Iterator<Integer> it = list.iterator();
	int sum = 0;
	while (it.hasNext()) {
		int num = it.next();
		if (num > 10) {
			sum += num;
		}
	}
	return sum;
}

В вышеуказанном подходе есть три основные проблемы:

  1. Мы просто хотим узнать сумму целых чисел, но мы также должны указать, как будет осуществляться итерация. Это также называется внешней итерацией, потому что клиентская программа обрабатывает алгоритм для итерации по списку.
  2. Программа последовательна по своей природе, и нет простого способа сделать это параллельно.
  3. Для выполнения даже простой задачи требуется много кода.

Для преодоления всех вышеуказанных недостатков был представлен Java 8 Stream API. Мы можем использовать Java Stream API для реализации внутренней итерации, что лучше, потому что фреймворк Java контролирует итерацию. Внутренняя итерация предоставляет несколько функций, таких как последовательное и параллельное выполнение, фильтрация на основе заданных критериев, отображение и т. д. Большинство аргументов методов Java 8 Stream API являются функциональными интерфейсами, поэтому лямбда-выражения прекрасно работают с ними. Давайте посмотрим, как мы можем написать вышеуказанную логику в единственном операторе с использованием Java Streams.

private static int sumStream(List<Integer> list) {
	return list.stream().filter(i -> i > 10).mapToInt(i -> i).sum();
}

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

Коллекции и Java Stream

A collection is an in-memory data structure to hold values and before we start using collection, all the values should have been populated. Whereas a java Stream is a data structure that is computed on-demand. Java Stream doesn’t store data, it operates on the source data structure (collection and array) and produce pipelined data that we can use and perform specific operations. Such as we can create a stream from the list and filter it based on a condition. Java Stream operations use functional interfaces, that makes it a very good fit for functional programming using lambda expression. As you can see in the above example that using lambda expressions make our code readable and short. Java 8 Stream internal iteration principle helps in achieving lazy-seeking in some of the stream operations. For example filtering, mapping, or duplicate removal can be implemented lazily, allowing higher performance and scope for optimization. Java Streams are consumable, so there is no way to create a reference to stream for future usage. Since the data is on-demand, it’s not possible to reuse the same stream multiple times. Java 8 Stream support sequential as well as parallel processing, parallel processing can be very helpful in achieving high performance for large collections. All the Java Stream API interfaces and classes are in the java.util.stream package. Since we can use primitive data types such as int, long in the collections using auto-boxing and these operations could take a lot of time, there are specific classes for primitive types – IntStream, LongStream and DoubleStream.

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

Некоторые из часто используемых функциональных интерфейсов в методах API потока Java 8:

  1. Функция и BiFunction: Функция представляет собой функцию, которая принимает один тип аргумента и возвращает другой тип аргумента. Function<T, R> – это обобщенная форма, где T – тип входа в функцию, а R – тип результата функции. Для работы с примитивными типами существуют специальные интерфейсы Function – ToIntFunction, ToLongFunction, ToDoubleFunction, ToIntBiFunction, ToLongBiFunction, ToDoubleBiFunction, LongToIntFunction, LongToDoubleFunction, IntToLongFunction, IntToDoubleFunction и т. д. Некоторые из методов потока, где используется Function или его примитивная специализация, включают:
    • <R> Stream<R> map(Function<? super T, ? extends R> mapper)
    • IntStream mapToInt(ToIntFunction<? super T> mapper) – аналогично для возвращающих long и double примитивных потоков.
    • IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper) – аналогично для long и double
    • <A> A[] toArray(IntFunction<A[]> generator)
    • <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
  2. Предикат и BiPredicate: Представляет собой предикат, по которому проверяются элементы потока. Используется для фильтрации элементов из потока Java. Как и в случае с Function, существуют интерфейсы, специфические для примитивов int, long и double. Некоторые методы Stream, где используются специализации Predicate или BiPredicate, включают:
    • Stream<T> filter(Predicate<? super T> predicate)
    • boolean anyMatch(Predicate<? super T> predicate)
    • boolean allMatch(Predicate<? super T> predicate)
    • boolean noneMatch(Predicate<? super T> predicate)
  3. Потребитель и БиПотребитель: Представляет собой операцию, которая принимает один входной аргумент и не возвращает результат. Его можно использовать для выполнения действий над всеми элементами потока Java. Некоторые из методов потока Java 8, где используются интерфейсы Consumer, BiConsumer или их примитивные специализированные интерфейсы, включают:
    • Stream<T> peek(Consumer<? super T> action)
    • void forEach(Consumer<? super T> action)
    • void forEachOrdered(Consumer<? super T> action)
  4. Поставщик: Поставщик представляет собой операцию, с помощью которой мы можем генерировать новые значения в потоке. Некоторые из методов в потоке, которые принимают аргумент Supplier, включают:
    • public static<T> Stream<T> generate(Supplier<T> s)
    • <R> R collect(Supplier<R> supplier,BiConsumer<R, ? super T> accumulator,BiConsumer<R, R> combiner)

java.util.Optional

Java Optional – это контейнерный объект, который может содержать значение, а может и не содержать. Если значение присутствует, метод isPresent() вернет true, и метод get() вернет это значение. Терминальные операции потока возвращают объект Optional. Некоторые из этих методов:

  • Optional<T> reduce(BinaryOperator<T> accumulator)
  • Optional<T> min(Comparator<? super T> comparator)
  • Optional<T> max(Comparator<? super T> comparator)
  • Optional<T> findFirst()
  • Optional<T> findAny()

java.util.Spliterator

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

Промежуточные и терминальные операции потока Java Stream

Java Stream API операции, возвращающие новый поток, называются промежуточными операциями. В большинстве случаев эти операции являются ленивыми, поэтому они начинают производить новые элементы потока и отправлять их на следующую операцию. Промежуточные операции никогда не являются окончательными операциями по производству результата. Часто используемые промежуточные операции – filter и map. Операции Java 8 Stream API, возвращающие результат или вызывающие побочный эффект. Как только на поток вызывается терминальный метод, он потребляет поток, и после этого мы не можем использовать поток. Терминальные операции жадные по своей природе, т.е. они обрабатывают все элементы в потоке перед возвратом результата. Часто используемые терминальные методы – forEach, toArray, min, max, findFirst, anyMatch, allMatch и т.д. Терминальные методы можно определить по типу возвращаемого значения – они никогда не вернут поток.

Промежуточные операции с коротким замыканием Java Stream

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

Примеры Java Stream

I have covered almost all the important parts of the Java 8 Stream API. It’s exciting to use this new API features and let’s see it in action with some java stream examples.

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

Существует несколько способов создания потоков Java из массивов и коллекций. Давайте рассмотрим это на простых примерах.

  1. Мы можем использовать Stream.of() для создания потока из аналогичного типа данных. Например, мы можем создать поток Java из целых чисел из группы объектов int или Integer.

    Stream<Integer> stream = Stream.of(1,2,3,4);
    
  2. Мы можем использовать Stream.of() с массивом объектов, чтобы получить поток. Обратите внимание, что он не поддерживает автоупаковку, поэтому мы не можем передавать массив примитивного типа.

    Stream<Integer> stream = Stream.of(new Integer[]{1,2,3,4}); 
    //работает нормально
    
    Stream<Integer> stream1 = Stream.of(new int[]{1,2,3,4}); 
    //Ошибка времени компиляции, Несовпадение типов: невозможно преобразовать из Stream<int[]> в Stream<Integer>
    
  3. Мы можем использовать Collection stream() для создания последовательного потока и parallelStream() для создания параллельного потока.

    List<Integer> myList = new ArrayList<>();
    for(int i=0; i<100; i++) myList.add(i);
    		
    // последовательный поток
    Stream<Integer> sequentialStream = myList.stream();
    		
    // параллельный поток
    Stream<Integer> parallelStream = myList.parallelStream();
    
  4. Мы можем использовать методы Stream.generate() и Stream.iterate() для создания потока.

    Stream<String> stream1 = Stream.generate(() -> {return "abc";});
    Stream<String> stream2 = Stream.iterate("abc", (i) -> i);
    
  5. Используя методы Arrays.stream() и String.chars().

    LongStream is = Arrays.stream(new long[]{1,2,3,4});
    IntStream is2 = "abc".chars();
    

Преобразование Java Stream в коллекцию или массив

Существует несколько способов, с помощью которых мы можем получить коллекцию или массив из потока Java.

  1. Мы можем использовать метод collect() в java Stream для получения списка (List), карты (Map) или множества (Set) из потока (stream).

    Stream<Integer> intStream = Stream.of(1,2,3,4);
    List<Integer> intList = intStream.collect(Collectors.toList());
    System.out.println(intList); //выводит [1, 2, 3, 4]
    
    intStream = Stream.of(1,2,3,4); //поток закрыт, поэтому нам нужно создать его заново
    Map<Integer,Integer> intMap = intStream.collect(Collectors.toMap(i -> i, i -> i+10));
    System.out.println(intMap); //выводит {1=11, 2=12, 3=13, 4=14}
    
  2. Мы можем использовать метод toArray() потока для создания массива из потока.

    Stream<Integer> intStream = Stream.of(1,2,3,4);
    Integer[] intArray = intStream.toArray(Integer[]::new);
    System.out.println(Arrays.toString(intArray)); //выводит [1, 2, 3, 4]
    

Промежуточные операции в потоке Java

Давайте рассмотрим примеры часто используемых промежуточных операций в потоке Java.

  1. Пример фильтрации потока: Мы можем использовать метод filter() для проверки элементов потока на условие и создания отфильтрованного списка.

    List<Integer> myList = new ArrayList<>();
    for(int i=0; i<100; i++) myList.add(i);
    Stream<Integer> sequentialStream = myList.stream();
    
    Stream<Integer> highNums = sequentialStream.filter(p -> p > 90); //фильтруем числа больше 90
    System.out.print("Числа больше 90=");
    highNums.forEach(p -> System.out.print(p+" "));
    //выводит "Числа больше 90=91 92 93 94 95 96 97 98 99 "
    
  2. Пример применения map() к потоку: Мы можем использовать метод map() для применения функций к потоку. Давайте посмотрим, как мы можем использовать его для применения функции преобразования в верхний регистр к списку строк.

    Stream<String> names = Stream.of("aBc", "d", "ef");
    System.out.println(names.map(s -> {
    		return s.toUpperCase();
    	}).collect(Collectors.toList()));
    //выводит [ABC, D, EF]
    
  3. Пример сортировки потока sorted(): Мы можем использовать sorted() для сортировки элементов потока, передавая аргумент Comparator.

    Stream<String> names2 = Stream.of("aBc", "d", "ef", "123456");
    List<String> reverseSorted = names2.sorted(Comparator.reverseOrder()).collect(Collectors.toList());
    System.out.println(reverseSorted); // [ef, d, aBc, 123456]
    
    Stream<String> names3 = Stream.of("aBc", "d", "ef", "123456");
    List<String> naturalSorted = names3.sorted().collect(Collectors.toList());
    System.out.println(naturalSorted); //[123456, aBc, d, ef]
    
  4. Пример flatMap() для потока: Мы можем использовать flatMap() для создания потока из потока списков. Давайте рассмотрим простой пример, чтобы прояснить это сомнение.

    Stream<List<String>> namesOriginalList = Stream.of(
    	Arrays.asList("Pankaj"), 
    	Arrays.asList("David", "Lisa"),
    	Arrays.asList("Amit"));
    //flat the stream from List<String> to String stream
    Stream<String> flatStream = namesOriginalList
    	.flatMap(strList -> strList.stream());
    
    flatStream.forEach(System.out::println);
    

Операции завершения потока Java Stream

Давайте рассмотрим несколько примеров завершающих операций потока Java.

  1. Пример использования reduce() для потока: Мы можем использовать reduce() для выполнения операции сокращения над элементами потока, используя ассоциативную функцию накопления, и вернуть Optional. Давайте посмотрим, как мы можем использовать ее для умножения целых чисел в потоке.

    Stream<Integer> numbers = Stream.of(1,2,3,4,5);
    		
    Optional<Integer> intOptional = numbers.reduce((i,j) -> {return i*j;});
    if(intOptional.isPresent()) System.out.println("Умножение = "+intOptional.get()); //120
    
  2. Пример использования count() для потока: Мы можем использовать эту завершающую операцию для подсчета количества элементов в потоке.

    Stream<Integer> numbers1 = Stream.of(1,2,3,4,5);
    		
    System.out.println("Количество элементов в потоке="+numbers1.count()); //5
    
  3. Пример использования forEach() в потоке: Это можно использовать для итерации по потоку. Мы можем использовать это вместо итератора. Давайте посмотрим, как его использовать для печати всех элементов потока.

    Stream<Integer> numbers2 = Stream.of(1,2,3,4,5);
    numbers2.forEach(i -> System.out.print(i+",")); //1,2,3,4,5,
    
  4. Примеры использования match() в потоке: Давайте посмотрим на некоторые примеры методов сопоставления в Stream API.

    Stream<Integer> numbers3 = Stream.of(1,2,3,4,5);
    System.out.println("Поток содержит 4? "+numbers3.anyMatch(i -> i==4));
    //Поток содержит 4? true
    
    Stream<Integer> numbers4 = Stream.of(1,2,3,4,5);
    System.out.println("Поток содержит все элементы меньше 10? "+numbers4.allMatch(i -> i<10));
    //Поток содержит все элементы меньше 10? true
    
    Stream<Integer> numbers5 = Stream.of(1,2,3,4,5);
    System.out.println("Поток не содержит 10? "+numbers5.noneMatch(i -> i==10));
    //Поток не содержит 10? true
    
  5. Пример использования метода findFirst() потока: Это операция терминального сокращения, давайте посмотрим, как мы можем использовать её, чтобы найти первую строку из потока, начинающуюся с буквы D.

    Stream<String> names4 = Stream.of("Pankaj","Amit","David", "Lisa");
    Optional<String> firstNameWithD = names4.filter(i -> i.startsWith("D")).findFirst();
    if(firstNameWithD.isPresent()){
    	System.out.println("First Name starting with D="+firstNameWithD.get()); //David
    }
    

Ограничения потокового API в Java 8

Потоковый API Java 8 вносит много новых возможностей для работы со списками и массивами, но у него также есть некоторые ограничения.

  1. Безсостоятельные лямбда-выражения: Если вы используете параллельный поток и лямбда-выражения имеют состояние, это может привести к случайным ответам. Давайте посмотрим на это на простой программе. StatefulParallelStream.java

    package com.journaldev.java8.stream;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Stream;
    
    public class StatefulParallelStream {
    
    	public static void main(String[] args) {
    
    		List<Integer> ss = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);
    		List<Integer> result = new ArrayList<Integer>();
    		 
    		Stream<Integer> stream = ss.parallelStream();
    		 
    		stream.map(s -> {
    		        synchronized (result) {
    		          if (result.size() < 10) {
    		            result.add(s);
    		          }
    		        }
    				return s;
    		    }).forEach( e -> {});
    		 System.out.println(result);   
    	}
    }
    

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

  2. Как только поток потреблен, его нельзя будет использовать позже. Как вы можете видеть в приведенных выше примерах, каждый раз я создаю поток.

  3. В Stream API есть много методов, и самая запутанная часть – это перегруженные методы. Это делает кривую обучения времязатратной.

Вот и все для примера учебника по потоку Java 8. Я с нетерпением жду, чтобы использовать эту функцию и сделать код более читаемым с лучшей производительностью благодаря параллельной обработке. Ссылка: Документация по Java Stream API

Source:
https://www.digitalocean.com/community/tutorials/java-8-stream