предисловие
Темой этой статьи является изучение содержания Java 8 Stream. Хотя сейчас выпущена Java 14, в настоящее время предприятия чаще всего используют Java 8. В краткосрочной перспективе мы сосредоточимся на использовании Java 8, а Stream — это Java 8. Очень важной частью этого является то, что освоение Stream API может сделать наш код чище и гибче.
Введение в потоки Java
Поток является важной функцией Java 8 и определяется в книге «Java 8 в действии» как: «последовательность элементов, сгенерированных из источника, который поддерживает операции обработки данных». Я думаю, что Stream также можно рассматривать как оболочку, обертывающую источник данных и выполняющую некоторые операции обработки над источником данных с помощью Stream. Следует отметить, что Stream не хранит данные, не является структурой данных и не изменяет базовый источник данных.
Промежуточная операция против конечной операции
Stream 接口定义在 java.util.stream.Stream 里 ,其中定义了很多操作,它们可以分为两大类:中间操作和终端操作。 Давайте посмотрим на следующий пример:
List<String> list = Arrays.asList("a1", "a2", "b1", "c1", "c2");
list.stream()
.filter(s->s.startsWith("c"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
//Output:
//C1
//C2
Можно увидеть два типа операций:
- filter, map и sorted являются промежуточными операциями
- forEach — терминальная операция
Промежуточные операции, такие как фильтр или сопоставление, возвращают другой поток, который может быть конвейеризирован. Операция терминала состоит в том, чтобы сгенерировать результат из конвейера потока, и он вернет void или значение, не являющееся потоком, например List, Integer.
forEach — терминальная операция, возвращающая void. в то же время,В Stream есть функция задержки, перед вызовом терминальной операции для вывода результата никакая промежуточная операция не обрабатывается, и она должна быть обработана сразу во время терминальной операции..
Streams VS Collections
Поток похож на Коллекции на поверхности, он может получать данные и работать с данными, но на самом деле он сильно отличается от Коллекции, вStream JavadocЕсть описание разницы. Здесь я также сделал таблицу, чтобы обобщить разницу между ними:
| Collections | Streams | |
|---|---|---|
| концепция | В основном используется для хранения данных | Не храните данные, в основном выполняйте вычислительные операции с данными |
| модификация данных | Элементы могут быть добавлены или удалены | Вы не можете добавлять или удалять элементы |
| повторять | Итерация должна выполняться извне, например, с помощью for-each | Используйте внутреннюю итерацию: сделайте итерацию за вас |
| траверс | можно пройти несколько раз | Его можно пройти только один раз или съесть только один раз. |
| действовать | Включите все элементы в расчет в начале | Отложенное выполнение, никакие промежуточные операции не обрабатываются до вызова терминальной операции |
Получить поток
Точно так же вStream JavadocНекоторые распространенные методы получения Stream также перечислены в:
- Получено с помощью методов stream() и parallelStream() коллекции
- Получить поток массива через Arrays.stream(Object[])
- Через статические методы Stream, такие как Stream.of(Object[]);IntStream.range(int, int) или Stream.iterate(Object, UnaryOperator)
- Статический метод Files.lines возвращает поток, содержащий все строки в файле.
- ......
Stream API предоставляет такие типы, как IntStream, LongStream и DoubleStream, которые специально используются для выполнения вычислений над значениями базового типа, что очень удобно. Для типов short, char, byte и boolean используйте IntStream, для типов float используйте DoubleStream.
Например, используйте метод IntStream.rang(int,int) для прямого создания диапазона целых чисел с шагом 1 следующим образом:
IntStream intStream = IntStream.range(1, 10); // 不包括上限
intStream.forEach(System.out::println);
Когда мы хотим создать бесконечный Stream, мы можем использовать два статических метода, предоставляемых Stream: generate и iterate.
- generate(Supplier s) : принимает объект интерфейса Supplier в качестве параметра для создания значений
- iTerate (Final T SEED, FINALOPERATOR F): принять «Seed» и функцию в качестве параметра
Пример, иллюстрирующий использование итерации:
Stream.iterate(3, i -> i * 2).limit(5).forEach(System.out::println);
Первый элемент равен 3, второй элемент равен 3*2 = 6, третий элемент равен: 6*2 = 12 и т. д., повторяя второй аргумент итерации до предыдущего значения. Здесь limit используется для усечения потока, чтобы он не превышал заданное количество элементов.
Stream API
В интерфейсе Stream определено много методов, таких как фильтр, сортировка, различение и т. д. Они относительно распространены и просты для понимания. Вот несколько более сложных методов.
map
Метод map() принимаетFunction<? super T, ? extends R>Функция, посредством которой элементы в потоке сопоставляются с другими формами элементов, то есть взаимно-однозначным сопоставлением, как в следующем примере:
List<String> list = Arrays.asList("abc1", "abc2");
list.stream()
.map(element -> element.substring(0, 3))
.forEach(System.out::println);
//Output:
//abc
//abc
Следующая диаграмма также показывает идеологическую карту (изображение изReactivexВеб-сайт):
flatMap
Как упоминалось ранее, метод map сопоставляет каждый элемент с другим значением один к одному.
в то время как метод flatMap принимаетFunction<? super T, ? extends Stream<? extends R>>Function, функция Function передается в универсальном T, генерируетStream<R>, вместо<R>,即每个元素转换成一个 Stream,Stream 又包含0、1、或多个元素。 Например следующий пример:
List<List<String>> names = Arrays.asList(
Arrays.asList("pjmike", "pj"),
Arrays.asList("Bob", "zhangshan"));
List<String> result = names.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
result.forEach(System.out::println);
//Output:
//pjmike pj Bob zhangshan
Первоначально вложенный список--List<List<String>>Каждый элемент представляет собой список, и FLATMAP может обрабатывать данные внутри каждого элемента списка, преобразовывать внутренние данные в один поток или выполнять дальнейшую обработку одного потока для получения нового потока. Наконец, в сочетании с одним потоком вместе, сгладить в поток.
Снова используйте картинку для иллюстрации идеи flatMap (картинка изReactivex):
Таким образом, метод flatMap позволяет вам заменить каждое значение в каждом потоке другим потоком, который может обрабатывать что-то более глубокое, а затем объединить все потоки в один поток, идея выравнивания.
reduce
В двух словах, сокращение — это операция агрегирования, и если вы хотите суммировать элементы или иным образом объединить элементы в потоке в одно значение, вы можете использовать метод уменьшения. reduce имеет три перегруженных метода, определенных следующим образом:
//第一个参数是初始值,第二个参数是累加器,BinaryOperator<T> 将两个元素结合起来产生一个新值
T reduce(T identity, BinaryOperator<T> accumulator);
//不接受初始值,只有一个累加器参数,会返回一个 Optional 对象
Optional<T> reduce(BinaryOperator<T> accumulator);
//入参为:初始值,累加器,组合器,用于并行流
<U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);
Примеры приведены ниже:
Optional<Integer> result1 = Stream.of(1, 2, 3, 4).reduce(Integer::sum); //使用方法引用
Integer result2 = Stream.of(1, 2, 3, 4).reduce(10, (x, y) -> x + y); //lambda表达式
System.out.println("result1: " + result1.get());
System.out.println("result2: " + result2);
List<User> users = Arrays.asList(new User("pj", 22), new User("pjmike", 21));
//对所有 User 的年龄求和
Integer ageSum = users.stream().reduce(0, (integer, user) -> integer + user.getAge(), Integer::sum);
System.out.println("the sum of ages: " + ageSum);
//Output:
//result1: 10
//result2: 20
//age sum : 43
Если тип параметра-аккумулятора не соответствует типу возвращаемого значения, требуется комбинатор. Например, в приведенном выше примере при суммировании возрастов всех пользователей типы параметров аккумулятора — Integer и User, но тип, возвращаемый аккумулятором, — это сумма целых чисел, и требуется комбинатор, иначе он не может быть скомпилирован.
Конечно, вы можете использовать его напрямуюmap-reduceобразом, который более читабелен, следующим образом:
List<User> users = Arrays.asList(new User("pj", 22), new User("pjmike", 21));
Integer reduce = users.stream()
.map(User::getAge)
.reduce(0, Integer::sum);
System.out.println(reduce);
//Output
//43
collect
collect — это терминальная операция, которая объединяет элементы потока в List или Set, Map и т. д.
collect принимает входной параметр как Collector (сборщик), а часто используемые сборщики строятся в классе фабрики Collections, который можно использовать напрямую. Например, в следующем распространенном примере toList() используется для преобразования потока в список:
List<Integer> list = Stream.of(1, 2, 3, 4)
.collect(Collectors.toList());
Другой пример — использовать метод Collections.groupingBy для группировки:
List<User> users = Arrays.asList(
new User("pj", 20),
new User("pjmike", 20),
new User("bob", 22));
Map<Integer, List<User>> map = users.stream()
.collect(Collectors.groupingBy(user -> user.getAge()));
map.forEach((age,user)-> System.out.printf("age %s: %s\n", age, user));
//Output:
//age 20: [StreamTest.User(name=pj, age=20), StreamTest.User(name=pjmike, age=20)]
//age 22: [StreamTest.User(name=bob, age=22)]
Для более интересного способа вы можете просмотреть исходный код класса фабрики коллекций, здесь он не представлен.
параллельный поток
Основные сведения, приведенные выше, — это последовательные потоки.По умолчанию мы вызываем метод коллекции stream() для создания последовательного потока. Мы увидели удобство использования потоков, но что, если потоки используются для параллельных вычислений?
Во-первых, вам нужно иметь параллельный поток.Параллельный поток — это поток, который делит содержимое на несколько блоков данных и обрабатывает каждую базу данных разными потоками.Нижний уровень использует ForkJoinPool..
Мы можем получить параллельный поток с помощью метода parallelStream() коллекции или вызвать метод parallel() для существующего последовательного потока, чтобы преобразовать последовательный поток в параллельный поток. Затем просто используйте параллельный поток, как если бы вы использовали последовательный поток.
boolean isParallel = IntStream.range(1, 5)
.parallel().isParallel();
System.out.println("isParallel: " + isParallel);
//Output:
//isParallel: true
Поскольку у меня мало опыта использования параллельных потоков, я представлю больше. Заинтересованные друзья могут читать книгу «Java8 Practive Combat», которая имеет подробное введение в параллельные потоки.
резюме
Это совместное использование потока Java 8. Если вы хотите узнать больше о потоке, вы можете прочитатьStream Javadocофициальная документация или«Ява 8 в действии»Эта книга.
Ссылки и благодарности
- stream javadoc
- The Java 8 Stream API Tutorial
- Mastering Lambdas: Java Programming in a Multicore World
- Difference Between Collections And Streams In Java
- Java 8 - An Introductory article to Java Streams API
- A Guide to Streams in Java 8: In-Depth Tutorial With Examples
- Java 8 в действии
- Java SE8 для занятых людей