Java Language
Streams
Поиск…
Вступление
Stream
представляет последовательность элементов и поддерживает различные виды операций для выполнения вычислений по этим элементам. С Java 8 интерфейс Collection
имеет два метода для генерации Stream
: stream()
и parallelStream()
. Операции Stream
являются промежуточными или конечными. Промежуточные операции возвращают Stream
поэтому несколько промежуточных операций могут быть привязаны до того, как Stream
будет закрыт. Операции с терминалом либо недействительны, либо возвращают результат без потока.
Синтаксис
- collection.stream ()
- Arrays.stream (массив)
- Stream.iterate (firstValue, currentValue -> nextValue)
- Stream.generate (() -> value)
- Stream.of (elementOfT [, elementOfT, ...])
- Stream.empty ()
- StreamSupport.stream (iterable.spliterator (), false)
Использование потоков
Stream
представляет собой последовательность элементов, на которых могут выполняться последовательные и параллельные операции агрегации. Любой данный Stream
может потенциально иметь неограниченное количество данных, проходящих через него. В результате данные, полученные от Stream
, обрабатываются индивидуально по мере их поступления, в отличие от пакетной обработки данных в целом. В сочетании с лямбда-выражениями они обеспечивают краткий способ выполнения операций над последовательностями данных с использованием функционального подхода.
Пример: ( см. Работу над Ideone )
Stream<String> fruitStream = Stream.of("apple", "banana", "pear", "kiwi", "orange");
fruitStream.filter(s -> s.contains("a"))
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
Выход:
ЯБЛОКО
БАНАН
ОРАНЖЕВЫЙ
ГРУША
Операции, выполняемые вышеуказанным кодом, можно суммировать следующим образом:
Создайте
Stream<String>
содержащий упорядоченный упорядоченныйStream
элементовString
для фруктов, используя статический заводский методStream.of(values)
.Операция
filter()
сохраняет только элементы, которые соответствуют заданному предикату (элементы, которые при проверке предикатом возвращают true). В этом случае он сохраняет элементы, содержащие"a"
. Предикат задается как лямбда-выражение .Операция
map()
преобразует каждый элемент с помощью заданной функции, называемой mapper. В этом случае каждаяString
фруктов преобразуется в его строчную версиюString
с использованием ссылки на методString::toUppercase
.Обратите внимание, что операция
map()
вернет поток с другим общим типом, если функция сопоставления возвращает тип, отличный от его входного параметра. Например, вStream<String>
вызов.map(String::isEmpty)
возвращаетStream<Boolean>
Операция
sorted()
сортирует элементыStream
соответствии с их естественным упорядочением (лексикографически, в случаеString
).Наконец, операция
forEach(action)
выполняет действие, действующее на каждый элементStream
, передавая его потребителю . В этом примере каждый элемент просто печатается на консоли. Эта операция является терминальной операцией, что делает невозможным ее повторное использование.Обратите внимание, что операции, определенные в
Stream
, выполняются из- за операции терминала. Без операции с терминалом поток не обрабатывается. Потоки нельзя использовать повторно. После вызова терминальной операции объектStream
становится непригодным.
Операции (как показано выше) соединены вместе, чтобы сформировать то, что можно рассматривать как запрос данных.
Закрытие потоков
Обратите внимание, что
Stream
как правило, не нужно закрывать. Требуется только закрыть потоки, которые работают на каналах ввода-вывода. Большинство типовStream
не работают на ресурсах и поэтому не требуют закрытия.
Интерфейс Stream
расширяет AutoCloseable
. Потоки могут быть закрыты вызовом метода close
или с помощью операторов try-with-resource.
Пример использования, когда Stream
должен быть закрыт, - это когда вы создаете Stream
строк из файла:
try (Stream<String> lines = Files.lines(Paths.get("somePath"))) {
lines.forEach(System.out::println);
}
Интерфейс Stream
также объявляет метод Stream.onClose()
который позволяет вам регистрировать обработчики Runnable
которые будут вызываться, когда поток закрыт. Пример использования - это то, где код, который создает поток, должен знать, когда он потребляется, чтобы выполнить некоторую очистку.
public Stream<String>streamAndDelete(Path path) throws IOException {
return Files.lines(path).onClose(() -> someClass.deletePath(path));
}
Обработчик запуска будет выполняться только в том случае, если метод close()
вызывается, явно или неявно, с помощью инструкции try-with-resources.
Порядок обработки
Обработка объекта Stream
может быть последовательной или параллельной .
В последовательном режиме элементы обрабатываются в порядке источника Stream
. Если Stream
упорядочен (например, реализация SortedMap
или List
), то гарантируется, что обработка будет соответствовать порядку источника. В других случаях, однако, следует соблюдать осторожность, чтобы не зависеть от порядка (см. keySet()
порядок итераций Java HashMap
keySet()
? ).
Пример:
List<Integer> integerList = Arrays.asList(0, 1, 2, 3, 42);
// sequential
long howManyOddNumbers = integerList.stream()
.filter(e -> (e % 2) == 1)
.count();
System.out.println(howManyOddNumbers); // Output: 2
Параллельный режим позволяет использовать несколько потоков на нескольких ядрах, но нет гарантии того, в каком порядке обрабатываются элементы.
Если вызывается несколько методов в последовательном Stream
, не каждый метод должен быть вызван. Например, если Stream
фильтруется и количество элементов сводится к единице, последующий вызов метода, такого как sort
, не будет выполняться. Это может увеличить производительность последовательного Stream
- оптимизация, которая невозможна при параллельном Stream
.
Пример:
// parallel
long howManyOddNumbersParallel = integerList.parallelStream()
.filter(e -> (e % 2) == 1)
.count();
System.out.println(howManyOddNumbersParallel); // Output: 2
Отличия от контейнеров (или коллекций )
Хотя некоторые действия могут выполняться как в контейнерах, так и в потоках, они в конечном итоге служат различным целям и поддерживают разные операции. Контейнеры больше сосредоточены на том, как хранятся элементы и как эти элементы могут быть доступны эффективно. С другой стороны, Stream
не обеспечивает прямого доступа и манипулирования его элементами; он больше предназначен для группы объектов как коллективного объекта и выполняет операции над этим объектом в целом. Stream
и Collection
- это отдельные абстракции высокого уровня для этих различных целей.
Соберите элементы потока в коллекцию
Собирайте с помощью toList()
и toSet()
Элементы из Stream
можно легко собрать в контейнер с помощью операции Stream.collect
:
System.out.println(Arrays
.asList("apple", "banana", "pear", "kiwi", "orange")
.stream()
.filter(s -> s.contains("a"))
.collect(Collectors.toList())
);
// prints: [apple, banana, pear, orange]
Другие экземпляры коллекции, такие как Set
, могут быть сделаны с использованием других встроенных методов Collectors
. Например, Collectors.toSet()
собирает элементы Stream
в Set
.
Явный контроль над реализацией List
или Set
Согласно документации Collectors#toList()
и Collectors#toSet()
, нет никаких гарантий по типу, изменчивости, сериализуемости или потокобезопасности возвращаемого List
или Set
.
Для явного контроля над возвращаемой реализацией вместо этого можно использовать Collectors#toCollection(Supplier)
, где данный поставщик возвращает новую и пустую коллекцию.
// syntax with method reference
System.out.println(strings
.stream()
.filter(s -> s != null && s.length() <= 3)
.collect(Collectors.toCollection(ArrayList::new))
);
// syntax with lambda
System.out.println(strings
.stream()
.filter(s -> s != null && s.length() <= 3)
.collect(Collectors.toCollection(() -> new LinkedHashSet<>()))
);
Сбор элементов с помощью toMap
Коллекционер накапливает элементы на карте, где ключ - это идентификатор студента, а значение - значение ученика.
List<Student> students = new ArrayList<Student>();
students.add(new Student(1,"test1"));
students.add(new Student(2,"test2"));
students.add(new Student(3,"test3"));
Map<Integer, String> IdToName = students.stream()
.collect(Collectors.toMap(Student::getId, Student::getName));
System.out.println(IdToName);
Выход :
{1=test1, 2=test2, 3=test3}
У Collectors.toMap есть другая реализация Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction)
. Функция mergeFunction используется в основном для выбора нового значения или сохранения старого значения, если ключ повторяется при добавлении нового члена в Map из списка.
Функция mergeFunction часто выглядит так: (s1, s2) -> s1
чтобы сохранить значение, соответствующее повторенному ключу, или (s1, s2) -> s2
чтобы добавить новое значение для повторного ключа.
Сбор элементов на карте коллекций
Пример: от ArrayList до Map <String, List <>>
Часто для этого требуется сделать карту списка из основного списка. Пример: от ученика списка, нам нужно составить карту списка предметов для каждого ученика.
List<Student> list = new ArrayList<>();
list.add(new Student("Davis", SUBJECT.MATH, 35.0));
list.add(new Student("Davis", SUBJECT.SCIENCE, 12.9));
list.add(new Student("Davis", SUBJECT.GEOGRAPHY, 37.0));
list.add(new Student("Sascha", SUBJECT.ENGLISH, 85.0));
list.add(new Student("Sascha", SUBJECT.MATH, 80.0));
list.add(new Student("Sascha", SUBJECT.SCIENCE, 12.0));
list.add(new Student("Sascha", SUBJECT.LITERATURE, 50.0));
list.add(new Student("Robert", SUBJECT.LITERATURE, 12.0));
Map<String, List<SUBJECT>> map = new HashMap<>();
list.stream().forEach(s -> {
map.computeIfAbsent(s.getName(), x -> new ArrayList<>()).add(s.getSubject());
});
System.out.println(map);
Выход:
{ Robert=[LITERATURE],
Sascha=[ENGLISH, MATH, SCIENCE, LITERATURE],
Davis=[MATH, SCIENCE, GEOGRAPHY] }
Пример: от ArrayList до Map <String, Map <>>
List<Student> list = new ArrayList<>();
list.add(new Student("Davis", SUBJECT.MATH, 1, 35.0));
list.add(new Student("Davis", SUBJECT.SCIENCE, 2, 12.9));
list.add(new Student("Davis", SUBJECT.MATH, 3, 37.0));
list.add(new Student("Davis", SUBJECT.SCIENCE, 4, 37.0));
list.add(new Student("Sascha", SUBJECT.ENGLISH, 5, 85.0));
list.add(new Student("Sascha", SUBJECT.MATH, 1, 80.0));
list.add(new Student("Sascha", SUBJECT.ENGLISH, 6, 12.0));
list.add(new Student("Sascha", SUBJECT.MATH, 3, 50.0));
list.add(new Student("Robert", SUBJECT.ENGLISH, 5, 12.0));
Map<String, Map<SUBJECT, List<Double>>> map = new HashMap<>();
list.stream().forEach(student -> {
map.computeIfAbsent(student.getName(), s -> new HashMap<>())
.computeIfAbsent(student.getSubject(), s -> new ArrayList<>())
.add(student.getMarks());
});
System.out.println(map);
Выход:
{ Robert={ENGLISH=[12.0]},
Sascha={MATH=[80.0, 50.0], ENGLISH=[85.0, 12.0]},
Davis={MATH=[35.0, 37.0], SCIENCE=[12.9, 37.0]} }
Чит-лист
Цель | Код |
---|---|
Сбор в List | Collectors.toList() |
Собирать в ArrayList с заранее заданным размером | Collectors.toCollection(() -> new ArrayList<>(size)) |
Собрать Set | Collectors.toSet() |
Собрать в Set с лучшей итерационной производительностью | Collectors.toCollection(() -> new LinkedHashSet<>()) |
Собрать в нечувствительный к регистру Set<String> | Collectors.toCollection(() -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER)) |
Соберите в EnumSet<AnEnum> (наилучшая производительность для перечислений) | Collectors.toCollection(() -> EnumSet.noneOf(AnEnum.class)) |
Соберите Map<K,V> с уникальными ключами | Collectors.toMap(keyFunc,valFunc) |
Карта MyObject.getter () для уникального объекта MyObject | Collectors.toMap(MyObject::getter, Function.identity()) |
Карта MyObject.getter () для нескольких объектов MyObjects | Collectors.groupingBy(MyObject::getter) |
Бесконечные потоки
Можно создать Stream
, который не заканчивается. Вызов метода терминала в бесконечном Stream
приводит к тому, что Stream
вводит бесконечный цикл. Метод limit
Stream
может использоваться для ограничения количества терминов Stream
которые обрабатывает Java.
В этом примере генерируется Stream
всех натуральных чисел, начиная с номера 1. Каждый последующий член Stream
является одним выше предыдущего. Вызывая метод ограничения этого Stream
, рассматриваются и печатаются только первые пять членов Stream
.
// Generate infinite stream - 1, 2, 3, 4, 5, 6, 7, ...
IntStream naturalNumbers = IntStream.iterate(1, x -> x + 1);
// Print out only the first 5 terms
naturalNumbers.limit(5).forEach(System.out::println);
Выход:
1
2
3
4
5
Другой способ генерации бесконечного потока - использовать метод Stream.generate . Этот метод принимает лямбда типа Поставщик .
// Generate an infinite stream of random numbers
Stream<Double> infiniteRandomNumbers = Stream.generate(Math::random);
// Print out only the first 10 random numbers
infiniteRandomNumbers.limit(10).forEach(System.out::println);
Потребительские потоки
Stream
будет пройден только тогда, когда есть операция терминала , например count()
, collect()
или forEach()
. В противном случае операция Stream
не будет выполнена.
В следующем примере терминальная операция не добавляется в Stream
, поэтому операция filter()
не будет вызываться и не будет выводиться вывод, потому что peek()
НЕ является терминальной операцией .
IntStream.range(1, 10).filter(a -> a % 2 == 0).peek(System.out::println);
Это последовательность Stream
с действительной работой терминала , поэтому производится выход.
Вы также можете использовать forEach
вместо peek
:
IntStream.range(1, 10).filter(a -> a % 2 == 0).forEach(System.out::println);
Выход:
2
4
6
8
После выполнения операции терминала Stream
потребляется и не может быть повторно использован.
Хотя данный объект потока нельзя использовать повторно, легко создать многоразовый Iterable
который делегирует потоковый конвейер. Это может быть полезно для возвращения измененного представления живого набора данных без необходимости собирать результаты во временную структуру.
List<String> list = Arrays.asList("FOO", "BAR");
Iterable<String> iterable = () -> list.stream().map(String::toLowerCase).iterator();
for (String str : iterable) {
System.out.println(str);
}
for (String str : iterable) {
System.out.println(str);
}
Выход:
Foo
бар
Foo
бар
Это работает, потому что Iterable
объявляет один абстрактный метод Iterator<T> iterator()
. Это делает его эффективным функциональным интерфейсом, реализованным лямбдой, которая создает новый поток для каждого вызова.
В общем, Stream
работает, как показано на следующем изображении:
ПРИМЕЧАНИЕ . Проверки аргументов всегда выполняются даже без операции терминала :
try {
IntStream.range(1, 10).filter(null);
} catch (NullPointerException e) {
System.out.println("We got a NullPointerException as null was passed as an argument to filter()");
}
Выход:
Мы получили исключение NullPointerException, поскольку значение null было передано как аргумент filter ()
Создание карты частоты
Сборщик groupingBy(classifier, downstream)
позволяет собирать элементы Stream
в Map
, классифицируя каждый элемент в группе и выполняя нисходящую операцию над элементами, классифицированными в той же группе.
Классическим примером этого принципа является использование Map
для подсчета появления элементов в Stream
. В этом примере классификатор - это просто функция идентификации, которая возвращает элемент as-is. Операция downstream подсчитывает количество равных элементов, используя counting()
.
Stream.of("apple", "orange", "banana", "apple")
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet()
.forEach(System.out::println);
Операция нисходящего потока сама является сборщиком ( Collectors.counting()
), который работает с элементами типа String и производит результат типа Long
. Результатом вызова метода collect
является Map<String, Long>
.
Это приведет к следующему результату:
банан = 1
оранжевый = 1
яблоко = 2
Параллельный поток
Примечание. Перед тем, как решить, какой Stream
использовать, пожалуйста, взгляните на поведение ParallelStream vs Sequential Stream .
Если вы хотите одновременно выполнять операции Stream
, вы можете использовать любой из этих способов.
List<String> data = Arrays.asList("One", "Two", "Three", "Four", "Five");
Stream<String> aParallelStream = data.stream().parallel();
Или же:
Stream<String> aParallelStream = data.parallelStream();
Чтобы выполнить операции, определенные для параллельного потока, вызовите оператор терминала:
aParallelStream.forEach(System.out::println);
(Возможен) выход из параллельного Stream
:
Три
четыре
Один
Два
5
Порядок может измениться, поскольку все элементы обрабатываются параллельно (что может ускорить выполнение). При заказе используйте параметр parallelStream
.
Эффективное воздействие
В случае задействования сети параллельный Stream
s может ухудшить общую производительность приложения, поскольку все параллельные Stream
используют общий пул потоков fork-join для сети.
С другой стороны, параллельный Stream
s может значительно повысить производительность во многих других случаях, в зависимости от количества доступных ядер в текущем CPU на данный момент.
Преобразование потока необязательно в поток значений
Возможно, вам потребуется конвертировать Stream
излучающий Optional
для Stream
значений, испускающий только значения из существующего Optional
. (т. е. без null
значения и не имея дело с Optional.empty()
).
Optional<String> op1 = Optional.empty();
Optional<String> op2 = Optional.of("Hello World");
List<String> result = Stream.of(op1, op2)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
System.out.println(result); //[Hello World]
Создание потока
Все java Collection<E>
имеют методы stream()
и parallelStream()
из которых можно построить Stream<E>
:
Collection<String> stringList = new ArrayList<>();
Stream<String> stringStream = stringList.parallelStream();
Stream<E>
может быть создан из массива с использованием одного из двух методов:
String[] values = { "aaa", "bbbb", "ddd", "cccc" };
Stream<String> stringStream = Arrays.stream(values);
Stream<String> stringStreamAlternative = Stream.of(values);
Разница между Arrays.stream()
и Stream.of()
заключается в том, что Stream.of()
имеет параметр varargs, поэтому его можно использовать как:
Stream<Integer> integerStream = Stream.of(1, 2, 3);
Есть также примитивные Stream
которые вы можете использовать. Например:
IntStream intStream = IntStream.of(1, 2, 3);
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);
Эти примитивные потоки также могут быть построены с использованием Arrays.stream()
:
IntStream intStream = Arrays.stream(new int[]{ 1, 2, 3 });
Можно создать Stream
из массива с указанным диапазоном.
int[] values= new int[]{1, 2, 3, 4, 5};
IntStream intStram = Arrays.stream(values, 1, 3);
Обратите внимание, что любой примитивный поток может быть преобразован в поток с коротким типом, используя метод boxed
:
Stream<Integer> integerStream = intStream.boxed();
Это может быть полезно в некоторых случаях, если вы хотите собирать данные, поскольку примитивный поток не имеет никакого метода collect
который принимает Collector
качестве аргумента.
Повторное использование промежуточных операций цепочки потоков
Поток закрывается, когда вызывается операция терминала. Повторное использование потока промежуточных операций, когда только операция терминала изменяется только. мы могли бы создать поставщика потока для создания нового потока со всеми уже созданными промежуточными операциями.
Supplier<Stream<String>> streamSupplier = () -> Stream.of("apple", "banana","orange", "grapes", "melon","blueberry","blackberry")
.map(String::toUpperCase).sorted();
streamSupplier.get().filter(s -> s.startsWith("A")).forEach(System.out::println);
// APPLE
streamSupplier.get().filter(s -> s.startsWith("B")).forEach(System.out::println);
// BANANA
// BLACKBERRY
// BLUEBERRY
int[]
массивы могут быть преобразованы в List<Integer>
с использованием потоков
int[] ints = {1,2,3};
List<Integer> list = IntStream.of(ints).boxed().collect(Collectors.toList());
Поиск статистики о числовых потоках
Java 8 предоставляет классы, называемые IntSummaryStatistics
, DoubleSummaryStatistics
и LongSummaryStatistics
которые предоставляют объект состояния для сбора статистики, такой как count
, min
, max
, sum
и average
.
List<Integer> naturalNumbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
IntSummaryStatistics stats = naturalNumbers.stream()
.mapToInt((x) -> x)
.summaryStatistics();
System.out.println(stats);
Это приведет к:
IntSummaryStatistics{count=10, sum=55, min=1, max=10, average=5.500000}
Получить фрагмент потока
Пример. Получите Stream
из 30 элементов, содержащий от 21 до 50 (включительно) элемент коллекции.
final long n = 20L; // the number of elements to skip
final long maxSize = 30L; // the number of elements the stream should be limited to
final Stream<T> slice = collection.stream().skip(n).limit(maxSize);
Заметки:
-
IllegalArgumentException
выбрасывается, еслиn
отрицательно илиmaxSize
отрицательный - обе
skip(long)
иlimit(long)
промежуточные операции - если поток содержит менее
n
элементов, тоskip(n)
возвращает пустой поток - как
skip(long)
иlimit(long)
являются дешевыми операциями на последовательных поточных трубопроводах, но могут быть довольно дорогими на упорядоченных параллельных трубопроводах
Конкатенация потоков
Объявление переменной для примеров:
Collection<String> abc = Arrays.asList("a", "b", "c");
Collection<String> digits = Arrays.asList("1", "2", "3");
Collection<String> greekAbc = Arrays.asList("alpha", "beta", "gamma");
Пример 1 - Объединение двух Stream
s
final Stream<String> concat1 = Stream.concat(abc.stream(), digits.stream());
concat1.forEach(System.out::print);
// prints: abc123
Пример 2 - Конкатенация более двух Stream
s
final Stream<String> concat2 = Stream.concat(
Stream.concat(abc.stream(), digits.stream()),
greekAbc.stream());
System.out.println(concat2.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma
В качестве альтернативы , чтобы упростить вложенный concat()
синтаксис Stream
ы также могут быть объединены с flatMap()
:
final Stream<String> concat3 = Stream.of(
abc.stream(), digits.stream(), greekAbc.stream())
.flatMap(s -> s);
// or `.flatMap(Function.identity());` (java.util.function.Function)
System.out.println(concat3.collect(Collectors.joining(", ")));
// prints: a, b, c, 1, 2, 3, alpha, beta, gamma
Будьте осторожны при построении Stream
из повторной конкатенации, потому что доступ к элементу глубоко конкатенированного Stream
может привести к глубоким цепочкам вызовов или даже StackOverflowException
.
IntStream to String
Java не имеет Char Stream , поэтому при работе со String
s и построении Stream
of Character
s, опция должна получить IntStream
кодовых точек с использованием String.codePoints()
. Таким образом, IntStream
можно получить следующим образом:
public IntStream stringToIntStream(String in) {
return in.codePoints();
}
Это немного больше, чтобы сделать конверсию другим способом, то есть IntStreamToString. Это можно сделать следующим образом:
public String intStreamToString(IntStream intStream) {
return intStream.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
}
Сортировка по потоку
List<String> data = new ArrayList<>();
data.add("Sydney");
data.add("London");
data.add("New York");
data.add("Amsterdam");
data.add("Mumbai");
data.add("California");
System.out.println(data);
List<String> sortedData = data.stream().sorted().collect(Collectors.toList());
System.out.println(sortedData);
Выход:
[Sydney, London, New York, Amsterdam, Mumbai, California]
[Amsterdam, California, London, Mumbai, New York, Sydney]
Также возможно использовать другой механизм сравнения, так как существует перегруженная sorted
версия, которая принимает в качестве аргумента компаратор.
Кроме того, вы можете использовать выражение лямбда для сортировки:
List<String> sortedData2 = data.stream().sorted((s1,s2) -> s2.compareTo(s1)).collect(Collectors.toList());
Это вывело бы [Sydney, New York, Mumbai, London, California, Amsterdam]
Вы можете использовать Comparator.reverseOrder()
чтобы иметь компаратор, который налагает reverse
сторону естественного упорядочения.
List<String> reverseSortedData = data.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
Потоки примитивов
Java предоставляет специализированные Stream
s для трех типов примитивов IntStream
(для int
s), LongStream
(для long
s) и DoubleStream
(для double
s). Помимо оптимизации реализаций их соответствующих примитивов, они также предоставляют несколько конкретных терминальных методов, как правило, для математических операций. Например:
IntStream is = IntStream.of(10, 20, 30);
double average = is.average().getAsDouble(); // average is 20.0
Соберите результаты потока в массив
Аналоговый, чтобы получить коллекцию для Stream
методом collect()
, массив может быть получен методом Stream.toArray()
:
List<String> fruits = Arrays.asList("apple", "banana", "pear", "kiwi", "orange");
String[] filteredFruits = fruits.stream()
.filter(s -> s.contains("a"))
.toArray(String[]::new);
// prints: [apple, banana, pear, orange]
System.out.println(Arrays.toString(filteredFruits));
String[]::new
- это особый вид ссылки на метод: ссылка на конструктор.
Поиск первого элемента, который соответствует предикату
Можно найти первый элемент Stream
который соответствует условию.
В этом примере мы найдем первое Integer
, квадрат которого превышает 50000
.
IntStream.iterate(1, i -> i + 1) // Generate an infinite stream 1,2,3,4...
.filter(i -> (i*i) > 50000) // Filter to find elements where the square is >50000
.findFirst(); // Find the first filtered element
Это выражение вернет OptionalInt
с результатом.
Обратите внимание, что с бесконечным Stream
Java будет проверять каждый элемент до тех пор, пока не найдет результат. С конечным Stream
, если Java исчерпывает элементы, но все равно не может найти результат, он возвращает пустой OptionalInt
.
Использование IntStream для перебора индексов
Stream
s элементов обычно не позволяет получить доступ к значению индекса текущего элемента. Чтобы перебирать массив или ArrayList
, имея доступ к индексам, используйте IntStream.range(start, endExclusive)
.
String[] names = { "Jon", "Darin", "Bauke", "Hans", "Marc" };
IntStream.range(0, names.length)
.mapToObj(i -> String.format("#%d %s", i + 1, names[i]))
.forEach(System.out::println);
Метод range(start, endExclusive)
возвращает другой ÌntStream
а mapToObj(mapper)
возвращает поток String
.
Выход:
# 1 Jon
# 2 Дарин
# 3 Бауке
# 4 Ханс
# 5 Марк
Это очень похоже на использование нормального for
цикла со счетчиком, но с выгодой конвейерных и распараллеливания:
for (int i = 0; i < names.length; i++) {
String newName = String.format("#%d %s", i + 1, names[i]);
System.out.println(newName);
}
Сгладить потоки с помощью flatMap ()
Stream
предметов, которые в свою очередь могут быть потоковыми, может быть сплющен в один непрерывный Stream
:
Массив списка элементов можно преобразовать в один список.
List<String> list1 = Arrays.asList("one", "two");
List<String> list2 = Arrays.asList("three","four","five");
List<String> list3 = Arrays.asList("six");
List<String> finalList = Stream.of(list1, list2, list3).flatMap(Collection::stream).collect(Collectors.toList());
System.out.println(finalList);
// [one, two, three, four, five, six]
Карта, содержащая Список элементов как значений, может быть сглажена в комбинированный список
Map<String, List<Integer>> map = new LinkedHashMap<>();
map.put("a", Arrays.asList(1, 2, 3));
map.put("b", Arrays.asList(4, 5, 6));
List<Integer> allValues = map.values() // Collection<List<Integer>>
.stream() // Stream<List<Integer>>
.flatMap(List::stream) // Stream<Integer>
.collect(Collectors.toList());
System.out.println(allValues);
// [1, 2, 3, 4, 5, 6]
List
Map
может быть сплющен в один непрерывный Stream
List<Map<String, String>> list = new ArrayList<>();
Map<String,String> map1 = new HashMap();
map1.put("1", "one");
map1.put("2", "two");
Map<String,String> map2 = new HashMap();
map2.put("3", "three");
map2.put("4", "four");
list.add(map1);
list.add(map2);
Set<String> output= list.stream() // Stream<Map<String, String>>
.map(Map::values) // Stream<List<String>>
.flatMap(Collection::stream) // Stream<String>
.collect(Collectors.toSet()); //Set<String>
// [one, two, three,four]
Создание карты на основе потока
Простой случай без дубликатов ключей
Stream<String> characters = Stream.of("A", "B", "C");
Map<Integer, String> map = characters
.collect(Collectors.toMap(element -> element.hashCode(), element -> element));
// map = {65=A, 66=B, 67=C}
Чтобы сделать вещи более декларативными, мы можем использовать статический метод в Function
интерфейсе - Function.identity()
. Мы можем заменить этот element -> element
лямбда- element -> element
Function.identity()
.
Случай, когда могут быть дубликаты ключей
В javadoc для Collectors.toMap
указано:
Если отображаемые ключи содержат дубликаты (в соответствии с
Object.equals(Object)
), при выполнении операции сбора генерируетсяIllegalStateException
. Если отображаемые ключи могут иметь дубликаты,toMap(Function, Function, BinaryOperator)
используйтеtoMap(Function, Function, BinaryOperator)
.
Stream<String> characters = Stream.of("A", "B", "B", "C");
Map<Integer, String> map = characters
.collect(Collectors.toMap(
element -> element.hashCode(),
element -> element,
(existingVal, newVal) -> (existingVal + newVal)));
// map = {65=A, 66=BB, 67=C}
BinaryOperator
передается в Collectors.toMap(...)
генерирует значение, которое нужно сохранить в случае столкновения. Оно может:
- возвращает старое значение, так что первое значение в потоке имеет приоритет,
- вернуть новое значение, так что последнее значение в потоке имеет приоритет или
- объединить старые и новые значения
Группировка по значению
Вы можете использовать Collectors.groupingBy
когда вам нужно выполнить эквивалент операции каскадирования базы данных «по группам». Чтобы проиллюстрировать это, создается следующая карта, в которой имена людей сопоставляются с фамилиями:
List<Person> people = Arrays.asList(
new Person("Sam", "Rossi"),
new Person("Sam", "Verdi"),
new Person("John", "Bianchi"),
new Person("John", "Rossi"),
new Person("John", "Verdi")
);
Map<String, List<String>> map = people.stream()
.collect(
// function mapping input elements to keys
Collectors.groupingBy(Person::getName,
// function mapping input elements to values,
// how to store values
Collectors.mapping(Person::getSurname, Collectors.toList()))
);
// map = {John=[Bianchi, Rossi, Verdi], Sam=[Rossi, Verdi]}
Генерация случайных строк с использованием потоков
Иногда бывает полезно создавать случайные Strings
, возможно, как идентификатор сеанса для веб-службы или начальный пароль после регистрации для приложения. Это может быть легко достигнуто с помощью Stream
s.
Сначала нам нужно инициализировать генератор случайных чисел. Чтобы повысить безопасность созданных String
s, рекомендуется использовать SecureRandom
.
Примечание . Создание SecureRandom
довольно дорогое, поэтому лучше всего сделать это только один раз и время от времени вызывать один из методов setSeed()
для его setSeed()
.
private static final SecureRandom rng = new SecureRandom(SecureRandom.generateSeed(20));
//20 Bytes as a seed is rather arbitrary, it is the number used in the JavaDoc example
При создании случайных String
s мы обычно хотим, чтобы они использовали только определенные символы (например, только буквы и цифры). Поэтому мы можем создать метод, возвращающий boolean
которое впоследствии может быть использовано для фильтрации Stream
.
//returns true for all chars in 0-9, a-z and A-Z
boolean useThisCharacter(char c){
//check for range to avoid using all unicode Letter (e.g. some chinese symbols)
return c >= '0' && c <= 'z' && Character.isLetterOrDigit(c);
}
Затем мы сможем использовать RNG для генерации случайной строки определенной длины, содержащей кодировку, которая передает нашу проверку useThisCharacter
.
public String generateRandomString(long length){
//Since there is no native CharStream, we use an IntStream instead
//and convert it to a Stream<Character> using mapToObj.
//We need to specify the boundaries for the int values to ensure they can safely be cast to char
Stream<Character> randomCharStream = rng.ints(Character.MIN_CODE_POINT, Character.MAX_CODE_POINT).mapToObj(i -> (char)i).filter(c -> this::useThisCharacter).limit(length);
//now we can use this Stream to build a String utilizing the collect method.
String randomString = randomCharStream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
return randomString;
}
Использование потоков для реализации математических функций
Stream
с, и особенно IntStream
с, является элегантным способом реализации суммирования условий (a). Диапазоны Stream
могут использоваться как границы суммирования.
Например, приближение Мадхавы к Pi дается формулой (Источник: wikipedia ):
Это можно вычислить с произвольной точностью. Например, на 101 срок:
double pi = Math.sqrt(12) *
IntStream.rangeClosed(0, 100)
.mapToDouble(k -> Math.pow(-3, -1 * k) / (2 * k + 1))
.sum();
Примечание: с double
точностью, выбирая верхнюю границу 29, достаточно, чтобы получить результат, который неотличим от Math.Pi
Использование потоков и ссылок на методы для записи самодокументирующих процессов
Ссылки на методы делают отличный самодокументирующий код, а использование ссылок на методы с помощью Stream
делает сложные процессы простыми для чтения и понимания. Рассмотрим следующий код:
public interface Ordered {
default int getOrder(){
return 0;
}
}
public interface Valued<V extends Ordered> {
boolean hasPropertyTwo();
V getValue();
}
public interface Thing<V extends Ordered> {
boolean hasPropertyOne();
Valued<V> getValuedProperty();
}
public <V extends Ordered> List<V> myMethod(List<Thing<V>> things) {
List<V> results = new ArrayList<V>();
for (Thing<V> thing : things) {
if (thing.hasPropertyOne()) {
Valued<V> valued = thing.getValuedProperty();
if (valued != null && valued.hasPropertyTwo()){
V value = valued.getValue();
if (value != null){
results.add(value);
}
}
}
}
results.sort((a, b)->{
return Integer.compare(a.getOrder(), b.getOrder());
});
return results;
}
Этот последний метод, переписанный с использованием ссылок Stream
и методов, намного читабельнее, и каждый шаг процесса быстро и легко понятен - он не просто короче, он также показывает с первого взгляда, какие интерфейсы и классы отвечают за код на каждом шаге:
public <V extends Ordered> List<V> myMethod(List<Thing<V>> things) {
return things.stream()
.filter(Thing::hasPropertyOne)
.map(Thing::getValuedProperty)
.filter(Objects::nonNull)
.filter(Valued::hasPropertyTwo)
.map(Valued::getValue)
.filter(Objects::nonNull)
.sorted(Comparator.comparing(Ordered::getOrder))
.collect(Collectors.toList());
}
Использование потоков Map.Entry для сохранения начальных значений после сопоставления
Когда у вас есть Stream
вам нужно сопоставить, но вы хотите сохранить начальные значения, вы можете сопоставить Stream
с Map.Entry<K,V>
с помощью утилиты, например:
public static <K, V> Function<K, Map.Entry<K, V>> entryMapper(Function<K, V> mapper){
return (k)->new AbstractMap.SimpleEntry<>(k, mapper.apply(k));
}
Затем вы можете использовать свой конвертер для обработки Stream
имеющего доступ к исходным и отображаемым значениям:
Set<K> mySet;
Function<K, V> transformer = SomeClass::transformerMethod;
Stream<Map.Entry<K, V>> entryStream = mySet.stream()
.map(entryMapper(transformer));
Затем вы можете продолжить обработку этого Stream
как обычно. Это позволяет избежать накладных расходов на создание промежуточной коллекции.
Категории операций по потоку
Операции потоков делятся на две основные категории: промежуточные и терминальные операции и две подкатегории, без гражданства и состояния.
Промежуточные операции:
Промежуточная операция всегда ленива , например простой Stream.map
. Он не вызывается до тех пор, пока поток фактически не будет потреблен. Это легко проверить:
Arrays.asList(1, 2 ,3).stream().map(i -> {
throw new RuntimeException("not gonna happen");
return i;
});
Промежуточные операции являются общими строительными блоками потока, закодированными после источника, и за ними обычно следует операция терминала, запускающая цепочку потоков.
Терминальные операции
Операции терминала - это то, что вызывает потребление потока. Некоторые из наиболее распространенных - Stream.forEach
или Stream.collect
. Они обычно размещаются после цепочки промежуточных операций и почти всегда стремятся .
Операции без гражданства
Безгражданство означает, что каждый элемент обрабатывается без контекста других элементов. Операции бездействия позволяют эффективно обрабатывать потоки с использованием памяти. Такие операции, как Stream.map
и Stream.filter
которые не требуют информации о других элементах потока, считаются апатридами.
Операции со штатом
Стойкость означает, что операция по каждому элементу зависит от (некоторых) других элементов потока. Для этого требуется сохранение состояния. Операции состояния могут прерываться с длинными или бесконечными потоками. Такие операции, как Stream.sorted
требуют, чтобы весь поток обрабатывался до того, как Stream.sorted
какой-либо элемент, который сломается в достаточно длинном потоке элементов. Это может быть продемонстрировано длинным потоком ( выполняется на свой страх и риск ):
// works - stateless stream
long BIG_ENOUGH_NUMBER = 999999999;
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).forEach(System.out::println);
Это вызовет Stream.sorted
памяти из-за Stream.sorted
:
// Out of memory - stateful stream
IntStream.iterate(0, i -> i + 1).limit(BIG_ENOUGH_NUMBER).sorted().forEach(System.out::println);
Преобразование итератора в поток
Используйте Spliterators.spliterator()
или Spliterators.spliteratorUnknownSize()
для преобразования итератора в поток:
Iterator<String> iterator = Arrays.asList("A", "B", "C").iterator();
Spliterator<String> spliterator = Spliterators.spliteratorUnknownSize(iterator, 0);
Stream<String> stream = StreamSupport.stream(spliterator, false);
Сокращение потока
Сокращение - это процесс применения бинарного оператора к каждому элементу потока, чтобы привести к одному значению.
Метод sum()
для IntStream
является примером сокращения; он применяет дополнение к каждому члену потока, что приводит к одному окончательному значению:
Это эквивалентно (((1+2)+3)+4)
Метод reduce
Stream позволяет создать индивидуальное сокращение. Для реализации метода sum()
можно использовать метод reduce
:
IntStream istr;
//Initialize istr
OptionalInt istr.reduce((a,b)->a+b);
Optional
версия возвращается, так что пустые потоки могут обрабатываться соответствующим образом.
Другим примером сокращения является объединение Stream<LinkedList<T>>
в один LinkedList<T>
:
Stream<LinkedList<T>> listStream;
//Create a Stream<LinkedList<T>>
Optional<LinkedList<T>> bigList = listStream.reduce((LinkedList<T> list1, LinkedList<T> list2)->{
LinkedList<T> retList = new LinkedList<T>();
retList.addAll(list1);
retList.addAll(list2);
return retList;
});
Вы также можете предоставить элемент идентификации . Например, элемент идентификации для сложения равен 0, как x+0==x
. Для умножения единичный элемент равен 1, так как x*1==x
. В вышеприведенном случае элемент идентификации представляет собой пустой LinkedList<T>
, потому что если вы добавляете пустой список в другой список, список, который вы добавляете, не изменяется:
Stream<LinkedList<T>> listStream;
//Create a Stream<LinkedList<T>>
LinkedList<T> bigList = listStream.reduce(new LinkedList<T>(), (LinkedList<T> list1, LinkedList<T> list2)->{
LinkedList<T> retList = new LinkedList<T>();
retList.addAll(list1);
retList.addAll(list2);
return retList;
});
Обратите внимание, что, когда предоставляется элемент идентификации, возвращаемое значение не обернуто в Optional
-if, вызываемый в пустом потоке, reduce()
вернет элемент идентификации.
Бинарный оператор также должен быть ассоциативным , что означает (a+b)+c==a+(b+c)
. Это связано с тем, что элементы могут быть уменьшены в любом порядке. Например, приведенное выше сведение может быть выполнено следующим образом:
Это сокращение эквивалентно записи ((1+2)+(3+4))
. Свойство ассоциативности также позволяет Java сокращать Stream
параллельно - часть потока может быть уменьшена каждым процессором, причем сокращение объединяет результат каждого процессора в конце.
Присоединение потока к одной строке
Часто используемый прецедент, создающий String
из потока, где элементы потока разделяются определенным символом. Для этого можно использовать метод Collectors.joining()
, как в следующем примере:
Stream<String> fruitStream = Stream.of("apple", "banana", "pear", "kiwi", "orange");
String result = fruitStream.filter(s -> s.contains("a"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.joining(", "));
System.out.println(result);
Выход:
APPLE, BANANA, ORANGE, PEAR
Метод Collectors.joining()
может также обслуживать pre- и postfixes:
String result = fruitStream.filter(s -> s.contains("e"))
.map(String::toUpperCase)
.sorted()
.collect(Collectors.joining(", ", "Fruits: ", "."));
System.out.println(result);
Выход:
Фрукты: APPLE, ORANGE, PEAR.