Почти каждое Java-приложение создает и обрабатывает коллекции. Коллекции являются основным требованием для многих задач программирования. Например, в системе банковских транзакций вам нужно создать коллекцию для хранения запросов пользователей на транзакцию, а затем выНужно пройти всю коллекциюНа этот раз, чтобы найти клиентов потратили в общей сложности сколько денег. Хотя коллекция очень важная, но в Java на наборе операций не идеальна.
Прежде всего, режим обработки коллекции должен иметь возможность выполнять такие операции, как запрос (самая крупная транзакция подряд) и группировка (общая сумма, используемая для потребления предметов первой необходимости), как при выполнении операций языка SQL. Большинство баз данных также могут иметь четкие инструкции по операциям, такие как «ВЫБЕРИТЕ идентификатор, MAX (значение) из транзакций». Оператор SQL-запроса позволяет найти самую большую транзакцию и ее идентификатор среди всех транзакций.
Как видите, нам не нужно реализовыватькакВычислите максимальное значение (например, циклы и отслеживание переменных, чтобы получить максимальное значение). мы просто должны выразить нашуожидатьКакие. Так почему же мы не можем разрабатывать и реализовывать коллекции так же, как работают запросы к базе данных?
Во-вторых, как мы должны эффективно обрабатывать большие коллекции данных? Идеальный способ ускорить обработку - использовать многоядерную архитектуру CPU, но написание параллельного кода сложно и без ошибок.
Java 8 сможет решить эту проблему идеально!Stream
Дизайн позволяет работать с данными декларативным способом. Потоки также позволяют использовать многоядерные архитектуры без написания многопоточного кода. Звучит здорово, не так ли? Это будет основное содержание, которое будет рассмотрено в этой серии статей.
Прежде чем мы рассмотрим, как мы используем потоки, давайте рассмотрим новую парадигму программирования с использованием потоков Java 8. Нам нужно выяснить все банковские транзакции, где типgrocery
и возвращает идентификаторы транзакций в порядке убывания суммы транзакции. В Java 7 нам нужно реализовать так:
List<Transaction> groceryTransactions = new Arraylist<>();
for(Transaction t: transactions){
if(t.getType() == Transaction.GROCERY){
groceryTransactions.add(t);
}
}
Collections.sort(groceryTransactions, new Comparator(){
public int compare(Transaction t1, Transaction t2){
return t2.getValue().compareTo(t1.getValue());
}
});
List<Integer> transactionIds = new ArrayList<>();
for(Transaction t: groceryTransactions){
transactionsIds.add(t.getId());
}
В Java 8 это возможно:
List<Integer> transactionsIds =
transactions.stream()
.filter(t -> t.getType() == Transaction.GROCERY)
.sorted(comparing(Transaction::getValue).reversed())
.map(Transaction::getId)
.collect(toList());
На следующем рисунке показан код реализации Java 8. Сначала мы используемstream()
Функция получает один из списка сведений о транзакции.stream
объект. Сопровождаемый некоторыми операциями (filter
,sorted
,map
,collect
) соединены вместе, образуя конвейер, который можно рассматривать как способ запроса данных, аналогичный базе данных.
Так что же делать с параллельным кодом? В Java8 очень просто: просто используйтеparallelStream()
заменятьstream()
Вот и все, как показано ниже, Stream API внутренне разложит ваши условия запроса на несколько ядер.
List<Integer> transactionsIds =
transactions.parallelStream()
.filter(t -> t.getType() == Transaction.GROCERY)
.sorted(comparing(Transaction::getValue).reversed())
.map(Transaction::getId)
.collect(toList());
Вы можете думать о потоке как об абстракции, которая повышает производительность агрегированных данных, предоставляя операции, подобные SQL, которые можно выразить с помощью лямбда-выражений.
В конце этой серии статей о потоках Java 8 вы будете использовать Stream API для написания кода, подобного приведенному выше, для реализации мощных возможностей запросов.
Начать работу с потоком
Начнем с теории. Какое определение для stream? Простое определение: «агрегатная операция над последовательностью элементов в источнике».
-
ряд элементов: Stream предоставляет интерфейс для набора элементов определенного типа. Но Stream на самом деле не хранит элементы, элементы вычисляются по требованию.
-
источник: поток может обрабатывать любой источник данных, например объединение, массив или ресурс ввода-вывода.
-
АгрегацияОперация: Stream поддерживает операции, аналогичные SQL, а обычные операции — это функциональные языки программирования, такие как фильтрация, сопоставление, сокращение, поиск, сопоставление, сортировка и т. д.
Потоковые операции также имеют два фундаментальных свойства, которые отличают их от операций с коллекциями:
-
трубопровод: Многие операции Stream возвращают сам объект потока. Это позволяет объединить все операции в более крупный конвейер. Это позволяет проводить определенные оптимизации, такие как отложенная загрузка и короткие циклы, о которых мы расскажем ниже.
-
Внутренние итерации: по сравнению с явной итерацией по коллекциям (внешняя итерация), операции Stream не требуют ручной итерации.
Давайте еще раз посмотрим на некоторые детали предыдущего кода:
Мы сначала проходимstream()
Функция получает объект Stream из списка транзакций. Этот источник данных представляет собой список транзакций, которые предоставят потоку массив элементов. Затем мы применяем некоторые операции агрегации столбцов к объекту Stream:filter
(элементы фильтра по заданному предикату),sorted
(сортировать по заданному компаратору) иmap
(для извлечения информации). Кромеcollect
Другие операции будут возвращать Stream, чтобы можно было сформировать конвейер для их соединения Мы можем думать об этой цепочке как об условии запроса к источнику.
Ничего существенного не вызывается до тех пор, пока не будет вызван метод collect.После вызова collect он начнет обработку конвейера и, наконец, вернет результат (результатом является список).
Прежде чем мы обсудим различные операции с потоками, давайте рассмотрим концептуальную разницу между потоками и коллекциями.
Stream VS Collection
И Collection, и Stream предоставляют некоторый интерфейс для некоторых элементов столбца. Разница между ними заключается в следующем: сбор связан с данными, поток связан с расчетом.
Подумайте о фильме на DVD, это коллекция, потому что она содержит все структуры данных. Однако фильмы в Интернете — это своего рода потоковые данные. Проигрывателям потокового мультимедиа нужно загрузить только несколько кадров, прежде чем пользователь сможет их просмотреть, а не все.
Проще говоря, Коллекция — это структура данных в памяти, которая содержит все значения в структуре данных — каждый элемент в Коллекции вычисляется перед добавлением в коллекцию. Вместо этого Stream — это структура данных, которая вычисляется только при необходимости.
Использование интерфейса Collection требует от пользователя выполнения итерации (например, использования foreach), которая называется внешней итерацией. Вместо этого Stream использует внутреннюю итерацию — он выполняет итерацию за вас и помогает с сортировкой. Вам просто нужно предоставить функцию, которая говорит, что вы хотите сделать. Следующий код использует коллекцию для внешней итерации:
List<String> transactionIds = new ArrayList<>();
for(Transaction t: transactions){
transactionIds.add(t.getId());
}
Следующий код использует Stream для внутренней итерации.
List<Integer> transactionIds =
transactions.stream()
.map(Transaction::getId)
.collect(toList());
Обработка данных с помощью Stream
Интерфейс Stream определяет ряд операций, которые можно разделить на две категории.
-
фильтровать, сортировать и отображать, которые могут быть объединены для формирования конвейера операций
-
сбор, операция, которая может закрыть конвейер, чтобы вернуть результат
Операции, которые можно связать, называются中间操作
. Вы можете объединить их, потому что они оба возвращают поток. Операция закрытия трубы называется终结操作
. Они могут создавать результат из конвейера, например список, целое число или даже пустоту.
Промежуточные операции фактически не выполняют никакой обработки, пока не будет вызван финализатор.; они ленивые". Потому что операции финализации обычно можно комбинировать, и финализированные операции выполняются все сразу.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
List<Integer> twoEvenSquares =
numbers.stream()
.filter(n -> {
System.out.println("filtering " + n);
return n % 2 == 0;
})
.map(n -> {
System.out.println("mapping " + n);
return n * n;
})
.limit(2)
.collect(toList());
Приведенный выше код вычислит первые два четных числа в наборе, и результат будет следующим:
filtering 1
filtering 2
mapping 2
filtering 3
filtering 4
mapping 4
Это связано с тем, что в limit(2) используется короткий цикл; нам нужно обработать только часть потока и вернуть результат. Это похоже на вычисление большого логического выражения: как только выражение возвращает false, мы можем заключить, что выражение вернет false, не вычисляя их все. Здесь операция limit возвращает поток размера 2. Кроме того, операция фильтра и операция сопоставления объединяются и передаются потоку.
Подводя итог тому, что мы узнали на данный момент: Потоковые операции состоят из трех вещей:
- запрос данныхисточник данных(например, сборник)
- серия трубПромежуточная операция
- Тот, который выполняет конвейер и выдает результатыокончание операции
Операции, предоставляемые Stream, можно разделить на следующие четыре категории:
-
фильтр: Существуют следующие типы операций фильтрации
-
filter(Predicate)
: использовать предикатjava.util.function.Predicate
В качестве аргумента возвращает поток, удовлетворяющий условию предиката. -
distinct
: возвращает поток без дублирующих элементов (в соответствии с реализацией равных) -
limit(n)
: возвращает поток до заданной длины -
skip(n)
: возвращает поток, игнорируя первые n
-
-
Найдите и сопоставьте: Обычный шаблон обработки данных заключается в том, чтобы определить, удовлетворяют ли некоторые элементы заданному атрибуту. можно использовать
anyMatch
,allMatch
, иnoneMatch
Действия, которые помогут вам достичь. им всем нужен одинpredicate
В качестве аргумента и возвращает логическое значение в результате (операция, поэтому они завершаются). Например, вы можете использовать allMatch, чтобы проверить, имеет ли car все элементы в потоке значение больше 100, как показано в приведенном ниже коде.
boolean expensive =
transactions.stream()
.allMatch(t -> t.getValue() > 100);
Кроме того, Stream предоставляетfindFirst
иfindAny
, вы можете получить любой элемент из Stream. Их можно связать с другими операциями Stream, такими какfilter
. И findFirst, и findAny возвращают необязательный объект следующим образом:
Optional<Transaction> =
transactions.stream()
.filter(t -> t.getType() == Transaction.GROCERY)
.findAny();
Optional<T>
Класс может содержать значение, которое существует или не существует. В приведенном ниже коде findAny может не возвращать тип транзакции Grocery. Необязательный имеет много способов проверить, существует ли элемент. Например, если транзакция существует, мы можем использовать связанные функции для обработки необязательных объектов.
transactions.stream()
.filter(t -> t.getType() == Transaction.GROCERY)
.findAny()
.ifPresent(System.out::println);
- карта: Stream поддерживает способ карты, карта использует функцию в качестве параметра, вы можете использовать карту для извлечения информации из одного элемента потока. В следующем примере мы вернемся к длине каждого слова в списке.
List<String> words = Arrays.asList("Oracle", "Java", "Magazine");
List<Integer> wordLengths =
words.stream()
.map(String::length)
.collect(toList());
Вы можете настроить более сложные запросы, такие как «максимальное значение идентификатора транзакции» или «вычислить сумму суммы транзакции». Этот процесс требует использованияreduce
Операции, сокращение может применить операцию к каждому элементу, зная вывод. сокращение также часто называют операцией складывания, потому что вы можете видеть, что эта операция похожа на складывание длинного листа бумаги (вашего потока) до тех пор, пока он не станет маленьким квадратом, что и является операцией складывания.
Взгляните на пример:
int sum = 0;
for (int x : numbers) {
sum += x;
}
Каждый элемент в списке многократно комбинируется с использованием знака «плюс», в результате чего получается результат. По сути, мы «сокращаем» данные в коллекции, которые в итоге становятся числом. Приведенный выше код имеет два параметра: начальное значение и оператор «+», который объединяет элементы в списке.
При использовании метода сокращения Stream мы можем использовать следующий код для добавления числовых элементов в коллекцию. Метод сокращения принимает два параметра:
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
- Начальное значение здесь равно 0.
- Добавление числа для подключения к новому значению возвращает BinaryOperator
Метод сокращения по существу абстрагирует повторяющийся шаблон. Другие запросы, такие как «вычислить продукт» или «вычислить максимальное значение», являются распространенными вариантами использования метода сокращения.
Числовой поток
Вы уже видели, что можно использовать метод сокращения для вычисления потока целых чисел. Однако мы сделали много нестандартных операций, чтобы многократно добавлять один объект Integer к другому. если мы позвонимsum
Разве метод не хорош? Как и в приведенном ниже коде, цель этого кода также более ясна.
int statement =
transactions.stream()
.map(Transaction::getValue)
.sum(); // 这里是会报错的
Для решения этой проблемы в Java 8 были введены три примитивных числовых интерфейса Stream.IntStream
, DoubleStream
, иLongStream
. Каждый из них может быть числовым Stream в int, double, long.
Вы можете использовать mapToInt, mapToDouble и mapToLong для преобразования общего потока в числовой поток Мы можем изменить приведенный выше код на следующий код. Конечно, вы можете использовать общие типы потоков вместо числовых потоков и использовать готовые операции.
int statementSum =
transactions.stream()
.mapToInt(Transaction::getValue)
.sum(); // 可以正确运行
Другое использование числового типа Stream — получение диапазона чисел. Например, вы можете захотеть сгенерировать все числа от 1 до 100. Java 8 представила два статических метода в IntStream, DoubleStream и LongStream, которые помогают генерировать диапазон.range
иrangeClosed
.
Оба метода принимают число в начале интервала в качестве первого параметра и число в конце интервала в качестве второго параметра. Но диапазон дальностиоткрытый интервалДа, rangeClosedзакрытый интервализ.下面是一个使用rangeClosed返回10到30之间的奇数的stream。
IntStream oddNumbers =
IntStream.rangeClosed(10, 30)
.filter(n -> n % 2 == 1);
Создать поток
Существует несколько способов создания потока. Вы уже знаете, что можете получить Stream из коллекции, и использовали числовой тип Stream. Вы можете создать поток с числами, массивами или файлами. Кроме того, вы даже можете создать бесконечный поток с помощью функции.
Создать поток из чисел или массивов очень просто: используйте статический метод Stream.of для чисел и статический метод Arrays.stream для массивов, например:
Stream<Integer> numbersFromValues = Stream.of(1, 2, 3, 4);
int[] numbers = {1, 2, 3, 4};
IntStream numbersFromArray = Arrays.stream(numbers);
Вы можете использовать статический метод Files.Lines для преобразования файла в поток. Например, следующий код вычисляет количество строк файлов.
long numberOfLines =
Files.lines(Paths.get(“yourFile.txt”), Charset.defaultCharset())
.count();
Бесконечный поток
К настоящему моменту вы знаете, что элементы Stream генерируются по запросу. Есть два статических методаStream.iterate
иStream.generate
Позволяет создать поток из функции, поскольку элементы подсчитываются по запросу, эти два метода всегда могут создавать элементы. Вот почему мы называем это бесконечным потоком: поток не имеет фиксированного размера, но он аналогичен потоку, созданному из коллекции фиксированного размера.
Следующий код используетсяiterate
Создал поток, содержащий число, кратное 10. Первый аргумент для итерации — это начальное значение, а второй — лямбда-выражение (типа UnaryOperator), используемое для создания каждого элемента.
Stream<Integer> numbers = Stream.iterate(0, n -> n + 10);
мы можем использоватьlimit
Операция преобразует бесконечный поток в поток фиксированного размера, например:
numbers.limit(5).forEach(System.out::println); // 0, 10, 20, 30, 40
Суммировать
В Java 8 появился Stream API, который позволяет реализовывать сложную обработку запросов данных. В этой статье мы увидели, что Stream поддерживает множество операций, таких как filter, mpa, reduce и iterate, которые могут помочь нам написать краткий код и реализовать сложные запросы обработки данных. Это сильно отличается от коллекций, использовавшихся до Java 8. Потоки имеют много преимуществ. Во-первых, Stream API оптимизирует запросы обработки данных, используя методы, которые внедряют ленивую загрузку и короткие циклы. Во-вторых, потоки могут автоматически работать параллельно, используя все преимущества многоядерных архитектур. В следующей статье мы рассмотрим более сложные операции, такие как flatMap и collect, так что следите за обновлениями.
Наконец
Спасибо, что прочитали. Если вам интересно, вы можете подписаться на общедоступную учетную запись WeChat, чтобы получать последние push-статьи.