Java 8 Stream API Аналогии в Котлине

Java 8 Stream API Аналогии в Котлине

1. Вступление

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

Мы рассмотрим, как мы можем достичь той же функциональности, используя идиомы Kotlin. Мы также рассмотрим функции, которые недоступны в простой Java.

2. Ява против Котлин

В Java 8 новый модный API можно использовать только при взаимодействии с экземплярамиjava.util.stream.Stream.

Хорошо то, что все стандартные коллекции - все, что реализуетjava.util.Collection - имеют конкретный методstream(), который может создавать экземплярStream.

Важно помнить, чтоStream не являетсяCollection.It does not implement java.util.Collection and it does not implement any of the normal semantics of Collections in Java.. Он больше похож на одноразовыйIterator в том смысле, что он получен изCollection и используется для работы с ним, выполняя операции над каждым видимым элементом.

In Kotlin, all collection types already support these operations без необходимости их предварительного преобразования. Преобразование требуется только в том случае, если семантика коллекции неверна - например,Set имеет уникальные элементы, но неупорядочен.

Одним из преимуществ этого является то, что нет необходимости в начальном преобразовании изCollection вStream, и нет необходимости в окончательном преобразовании изStream обратно в коллекцию - с использованиемcollect() звонки.

Например, в Java 8 нам нужно написать следующее:

someList
  .stream()
  .map() // some operations
  .collect(Collectors.toList());

Эквивалент в Kotlin очень прост:

someList
  .map() // some operations

Additionally, Java 8 Streams are also non-reusable. После того, какStream израсходован, его нельзя использовать снова.

Например, следующее не будет работать:

Stream someIntegers = integers.stream();
someIntegers.forEach(...);
someIntegers.forEach(...); // an exception

В Kotlin тот факт, что это все обычные коллекции, означает, что эта проблема никогда не возникает. Intermediate state can be assigned to variables and shared quickly, и работает, как и следовало ожидать.

3. Ленивые последовательности

Одна из ключевых особенностей Java 8Streams заключается в том, что они вычисляются лениво. Это означает, что не будет выполнено больше работы, чем необходимо.

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

Например,IntStream.generate создаст потенциально бесконечное количество целых чиселStream. Если мы вызовем для негоfindFirst(), мы получим первый элемент и не запустим бесконечный цикл.

In Kotlin, collections are eager, rather than lazy. Исключением являетсяSequence, который выполняет ленивое вычисление.

Это важное различие, которое следует отметить, как показано в следующем примере:

val result = listOf(1, 2, 3, 4, 5)
  .map { n -> n * n }
  .filter { n -> n < 10 }
  .first()

Версия Kotlin для этого выполнит пять операцийmap(), пять операцийfilter(), а затем извлечет первое значение. Версия Java 8 выполнит только одинmap() и одинfilter(), потому что с точки зрения последней операции больше не требуется.

All collections in Kotlin can be converted to a lazy sequence using the asSequence() method.

ИспользованиеSequence вместоList в приведенном выше примере выполняет то же количество операций, что и в Java 8.

4. Java 8Stream Операции

В Java 8 операцииStream разбиты на две категории:

  • средний и

  • Терминал

Промежуточные операции, по сути, лениво преобразуют одинStream в другой - например,Stream всех целых чисел вStream всех четных целых чисел.

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

В Котлине нет такого различия. Вместо этогоthese are all just functions that take the collection as input and produce a new output.

Обратите внимание, что если мы используем активную коллекцию в Kotlin, эти операции оцениваются немедленно, что может быть удивительно по сравнению с Java. If we need it to be lazy, remember to convert to a Sequence first.с

4.1. Промежуточные операции

Almost all intermediate operations from the Java 8 Streams API have equivalents in Kotlin. Однако это не промежуточные операции - за исключением случая классаSequence - поскольку они приводят к полностью заполненным коллекциям в результате обработки входной коллекции.

Среди этих операций есть несколько, которые работают точно так же -filter(),map(),flatMap(),distinct() иsorted() - а некоторые работают то же самое только с другими именами -limit() теперьtake, аskip() теперьdrop(). Например:

val oddSquared = listOf(1, 2, 3, 4, 5)
  .filter { n -> n % 2 == 1 } // 1, 3, 5
  .map { n -> n * n } // 1, 9, 25
  .drop(1) // 9, 25
  .take(1) // 9

Это вернет единственное значение «9» - 3².

Some of these operations also have an additional version – suffixed with the word “To” - выводит в предоставленную коллекцию вместо создания новой.

Это может быть полезно для обработки нескольких входных коллекций в одной выходной коллекции, например:

val target = mutableList()
listOf(1, 2, 3, 4, 5)
  .filterTo(target) { n -> n % 2 == 0 }

Это вставит значения «2» и «4» в список «target».

The only operation that does not normally have a direct replacement is peek() - используется в Java 8 для перебора записей вStream в середине конвейера обработки без прерывания потока.

Если мы используем ленивуюSequence вместо активной коллекции, тогда существует функцияonEach(), которая напрямую заменяет функциюpeek. Это существует только в этом одном классе, и поэтому мы должны знать, какой тип мы используем для его работы.

There are also some additional variations on the standard intermediate operations that make life easier. Например, операцияfilter имеет дополнительные версииfilterNotNull(),filterIsInstance(),filterNot() иfilterIndexed().

Например:

listOf(1, 2, 3, 4, 5)
  .map { n -> n * (n + 1) / 2 }
  .mapIndexed { (i, n) -> "Triangular number $i: $n" }

Это даст первые пять треугольных чисел в форме «Треугольное число 3: 6»

Еще одно важное отличие заключается в способе работы операцииflatMap. В Java 8 эта операция требуется для возврата экземпляраStream, тогда как в Kotlin она может возвращать любой тип коллекции. Это облегчает работу.

Например:

val letters = listOf("This", "Is", "An", "Example")
  .flatMap { w -> w.toCharArray() } // Produces a List
  .filter { c -> Character.isUpperCase(c) }

В Java 8 вторая строка должна быть заключена вArrays.toStream(), чтобы это работало.

4.2. Терминальные операции

Все стандартные терминальные операции из Java 8 Streams API имеют прямые замены в Kotlin, за единственным исключениемcollect.

У некоторых из них есть разные имена:

  • anyMatch()any()

  • allMatch()all()

  • noneMatch()none()

У некоторых из них есть дополнительные варианты для работы с различиями в Kotlin - естьfirst() иfirstOrNull(), гдеfirst выдает, если коллекция пуста, но в противном случае возвращает тип, не допускающий значения NULL.

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

Это позволяет предоставить произвольныйCollector, который будет предоставлен каждому элементу в коллекции и будет производить какой-либо вывод. Они используются из вспомогательного классаCollectors, но при необходимости мы можем написать свои собственные.

In Kotlin there are direct replacements for almost all of the standard collectors available directly as members on the collection object itself - нет необходимости в дополнительной ступени при наличии коллектора.

Единственным исключением здесь являются методыsummarizingDouble /summarizingInt /summarizingLong, которые производят среднее значение, счет, минимум, максимум и суммирование за один раз. Каждый из них может быть произведен индивидуально - хотя это, очевидно, имеет более высокую стоимость.

В качестве альтернативы мы можем управлять им с помощью цикла for-each и при необходимости обрабатывать его вручную -it is unlikely we will need all 5 of these values at the same time, so we only need to implement the ones that are important.

5. Дополнительные операции в Котлине

Kotlin добавляет к коллекциям некоторые дополнительные операции, которые невозможны в Java 8 без их реализации.

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

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

Есть также некоторые операции, которые явно используют преимущества нулевой безопасности Kotlin - например; мы можем выполнитьfilterNotNull() дляList<String?>, чтобы вернутьList<String>, где все нули удалены.

Фактические дополнительные операции, которые могут быть выполнены в Kotlin, но не в Java 8 Streams, включают:

  • zip() иunzip() - используются для объединения двух коллекций в одну последовательность пар и, наоборот, для преобразования коллекции пар в две коллекции

  • associate - используется для преобразования коллекции в карту путем предоставления лямбда-выражения для преобразования каждой записи в коллекции в пару ключ / значение в результирующей карте

Например:

val numbers = listOf(1, 2, 3)
val words = listOf("one", "two", "three")
numbers.zip(words)

Это даетList<Pair<Int, String>> со значениями1 to “one”, 2 to “two” и3 to “three”.

val squares = listOf(1, 2, 3, 4,5)
  .associate { n -> n to n * n }

Это даетMap<Int, Int>, где ключи - это числа от 1 до 5, а значения - квадраты этих значений.

6. Резюме

Большинство потоковых операций, к которым мы привыкли в Java 8, можно напрямую использовать в Kotlin в стандартных классах Collection, без необходимости сначала конвертировать вStream.

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

Тем не менее, Котлин стремится по умолчанию, а не ленивый. Это может привести к дополнительной работе, если мы не будем осторожны с типами коллекций, которые используются.