На этом шаге мы рассмотрим тип отложенной коллекции.
Ранее вы познакомились с типами коллекций List, Set, Map. Эти коллекции известны как готовые коллекции. Когда создается экземпляр любого из этих типов, он сразу содержит все значения элементов коллекции и дает доступ к ним.
Есть другой вид коллекций - отложенные коллекции. Вы узнали об отложенной инициализации, когда переменные инициализируются при первом обращении к ним. Отложенные типы коллекций, как и отложенная инициализация, позволяют увеличить производительность, особенно при работе с большими коллекциями, потому что значения в таких коллекциях создаются только по необходимости.
В Kotlin имеется встроенный тип Sequance отложенной коллекции. Последовательности не поддерживают доступ к содержимому по индексам и не контролируют свой размер. Более того, при работе с последовательностью, есть вероятность получить бесконечное количество значений, потому что нет ограничений на количество элементов, которое может сгенерировать последовательность.
Для последовательности вы объявляете функцию-итератор, которая вызывается каждый раз, когда необходимо новое значение. Один из способов объявить последовательность и ее итератор - это использовать встроенную в Kotlin функцию generateSquence(). Функция generateSquence() принимает начальное значение, которое будет точкой старта для последовательности. При обращении к последовательности в функциональном стиле generateSquence() вызовет указанный вами итератор для получения следующего значения. Например:
generateSequence(0) { it + 1 } .onEach { println("The Count says: $it, ah ah ah!") }
Если вы запустите этот фрагмент кода, то он будет выполняться бесконечно.
Чем же хороша отложенная коллекция и когда стоит отдать ей предпочтение? Вернемся к примеру с поиском простых чисел в примере из 224 шага. Предположим, что вы бы хотели добавить вывод первых N простых чисел, например 1000. Первая попытка может выглядеть так:
// Расширение для Int, которое проверяет, является ли число простым fun Int.isPrime(): Boolean { (2 until this).map { if (this % it == 0) { return false // Не простое! } } return true } val toList = (1..5000).toList().filter { it.isPrime() }.take(1000)
Проблема с этой реализацией заключается в том, что непонятно, сколько чисел надо проверить, прежде чем наберется 1000 простых чисел. Реализация берет наугад 5000 чисел, но этого может быть недостаточно (фактически вы получите 669 простых чисел).
Это отличный случай для использования отложенной коллекции в цепочке функций. Отложенная коллекция идеально подходит, потому что не требует определять количество элементов последовательности для проверки:
val oneThousandPrimes = generateSequence(3) {
value -> value + 1
}.filter { it.isPrime() }.take(1000)
В этом решении generateSequence() последовательно создает новые значения, начиная с 3 (начальное значение) и прибавляя каждый раз по единице. Дальше с помощью расширения isPrime() происходит фильтрация значений. Так продолжается, пока не будет создано 1000 элементов. Так как нет способа узнать, как много элементов будет проверено, отложенное создание новых элементов будет происходить, пока условие функции take() не будет удовлетворено.
В большинстве случаев коллекции, с которыми вы работаете, будут небольшими, включающими не более 1000 элементов. В подобных случаях беспокоиться о том, что лучше использовать для такого ограниченного набора элементов - последовательность или просто список, не имеет особого смысла, так как разница в производительности этих типов составит всего несколько наносекунд. Но с огромными коллекциями, состоящими из сотен тысяч элементов, добиться улучшения производительности за счет смены типа коллекции вполне реально. В подобных случаях преобразовать список в последовательность довольно просто:
val listOfNumbers = (0 until 10000000).toList() val sequenceOfNumbers = listOfNumbers.asSequence()
Парадигма функционального программирования может требовать постоянного создания новых коллекций, а последовательности предлагают масштабируемый механизм для работы с большими коллекциями.
В этих шагах вы познакомились с базовыми инструментами функционального программирования, такими как map(), flatMap() и filter(), которые упрощают работу с данными. Также вы видели, как использовать последовательности для эффективной работы с постоянно растущими объемами данных.
В следующих шагах вы узнаете, как код на Kotlin взаимодействует с кодом на Java, как вызвать код на Java из кода на Kotlin, и наоборот.
На следующем шаге мы рассмотрим профилирование.