Фильтрация коллекций Kotlin

1. Общий обзор

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

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

Все эти методы возвращают новую коллекцию, оставляя исходную коллекцию неизмененной.

Для выполнения некоторых фильтров мы будем использовать лямбда-выражения. Чтобы узнать больше о лямбда-выражениях, ознакомьтесь с нашей статьей о лямбде в Kotlin здесь.

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

2. Drop

Мы начнем с простого способа сокращения коллекции. Удаление позволяет нам взять часть коллекции и вернуть новый список, в котором отсутствует количество элементов, указанных в number.:

@Test
fun whenDroppingFirstTwoItemsOfArray_thenTwoLess() {
val array = arrayOf(1, 2, 3, 4)
val result = array.drop(2)
val expected = listOf(3, 4)

assertIterableEquals(expected, result)
}

С другой стороны, если мы хотим удалить последние n элементов, мы вызываем drop Last:

@Test
fun givenArray_whenDroppingLastElement_thenReturnListWithoutLastElement() {
val array = arrayOf("1", "2", "3", "4")
val result = array.dropLast(1)
val expected = listOf("1", "2", "3")

assertIterableEquals(expected, result)
}

Теперь мы рассмотрим наше первое условие фильтрации, которое содержит предикат.

Эта функция возьмет наш код и будет работать в обратном направлении по списку, пока мы не дойдем до элемента, который не соответствует условию:

@Test
fun whenDroppingLastUntilPredicateIsFalse_thenReturnSubsetListOfFloats() {
val array = arrayOf(1f, 1f, 1f, 1f, 1f, 2f, 1f, 1f, 1f)
val result = array.dropLastWhile { it == 1f }
val expected = listOf(1f, 1f, 1f, 1f, 1f, 2f)

assertIterableEquals(expected, result)
}

drop Last While удалил последние три элемента 1f из списка, поскольку метод циклически просматривал каждый элемент до тех пор, пока не был найден первый экземпляр, в котором элемент массива не был равен 1f.

Метод прекращает удаление элементов, как только элемент не удовлетворяет условию предиката.

dropWhile - это еще один фильтр, который использует предикат, но dropWhile работает с индексом 0 -> n, а drop Last While - с индексом n -> 0.

Если мы попытаемся удалить больше элементов, чем содержится в коллекции, мы просто получим пустой список.

3. Take

Очень похоже на drop, take сохранит элементы в соответствии с заданным индексом или предикатом:

@Test
fun `when predicating on 'is String', then produce list of array up until predicate is false`() {
val originalArray = arrayOf("val1", 2, "val3", 4, "val5", 6)
val actualList = originalArray.takeWhile { it is String }
val expectedList = listOf("val1")

assertIterableEquals(expectedList, actualList)
}

Разница между drop и take заключается в том, что drop удаляет элементы, в то время как take сохраняет их.

Попытка взять больше элементов, чем доступно в коллекции, просто вернет список того же размера, что и исходная коллекция

Здесь важно отметить, что takeIf не является методом сбора. takeIf использует предикат, чтобы определить, возвращать нулевое значение или нет – например, необязательный#filter.

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

4. Filter

Filter создает новый список на основе предоставленного предиката:

@Test
fun givenAscendingValueMap_whenFilteringOnValue_ThenReturnSubsetOfMap() {
val originalMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3)
val filteredMap = originalMap.filter { it.value < 2 }
val expectedMap = mapOf("key1" to 1)

assertTrue { expectedMap == filteredMap }
}

При фильтрации у нас есть функция, которая позволяет нам накапливать результаты наших фильтров для разных массивов. Она называется filter To и преобразует копию изменяемого списка в данный изменяемый массив.

Это позволяет нам собирать несколько коллекций и фильтровать их в единую накопительную коллекцию.

В этом примере используются массив, последовательность и список.

Затем ко всем трем предикатам применяется один и тот же предикат для фильтрации простых чисел, содержащихся в каждой коллекции:

@Test
fun whenFilteringToAccumulativeList_thenListContainsAllContents() {
val array1 = arrayOf(90, 92, 93, 94, 92, 95, 93)
val array2 = sequenceOf(51, 31, 83, 674_506_111, 256_203_161, 15_485_863)
val list1 = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
val primes = mutableListOf<Int>()

val expected = listOf(2, 3, 5, 7, 31, 83, 15_485_863, 256_203_161, 674_506_111)

val primeCheck = { num: Int -> Primes.isPrime(num) }

array1.filterTo(primes, primeCheck)
list1.filterTo(primes, primeCheck)
array2.filterTo(primes, primeCheck)

primes.sort()

assertIterableEquals(expected, primes)
}

Filter с предикатом или без него также хорошо работают с map:

val originalMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3)
val filteredMap = originalMap.filter { it.value < 2 }

Очень полезной парой методов фильтрации является filterNotNull и filterNotNullTo, которые просто отфильтровывают все нулевые элементы.

Наконец, если нам когда-нибудь понадобится использовать индекс элемента коллекции,
filter Indexed и filter Индексированный, чтобы обеспечить возможность использования лямбда-предиката как с элементом, так и с его индексом позиции.

5. Slice

Мы также можем использовать диапазон для выполнения среза. Чтобы выполнить срез, мы просто определяем диапазон, который наш срез хочет извлечь:

@Test
fun whenSlicingAnArrayWithDotRange_ThenListEqualsTheSlice() {
val original = arrayOf(1, 2, 3, 2, 1)
val actual = original.slice(3 downTo 1)
val expected = listOf(2, 3, 2)

assertIterableEquals(expected, actual)
}

Срез может быть направлен как вверх, так и вниз.

При использовании диапазонов мы также можем задать размер шага диапазона.

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

Однако выход за пределы коллекции с использованием диапазона с шагами
может вызвать исключение ArrayIndexOutOfBoundsException:

@Test
fun whenSlicingBeyondRangeOfArrayWithStep_thenOutOfBoundsException() {
assertThrows(ArrayIndexOutOfBoundsException::class.java) {
val original = arrayOf(12, 3, 34, 4)
original.slice(3..8 step 2)
}
}

6. Distinct

Другой фильтр, который мы рассмотрим в этой статье, - distinct. Мы можем использовать этот метод для сбора уникальных объектов из нашего списка:

@Test
fun whenApplyingDistinct_thenReturnListOfNoDuplicateValues() {
val array = arrayOf(1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 5, 6, 7, 8, 9)
val result = array.distinct()
val expected = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)

assertIterableEquals(expected, result)
}

У нас также есть возможность использовать функцию selector. Selector возвращает значение, которое мы собираемся оценить на уникальность.

Мы реализуем небольшой класс данных SmallClass, чтобы изучить работу с объектом в selector:

data class SmallClass(val key: String, val num: Int)

используя массив небольшого класса:

val original = arrayOf(
SmallClass("key1", 1),
SmallClass("key2", 2),
SmallClass("key3", 3),
SmallClass("key4", 3),
SmallClass("er", 9),
SmallClass("er", 10),
SmallClass("er", 11))

Мы можем использовать различные поля в рамках distinct By:

val actual = original.distinctBy { it.key }
val expected = listOf(
SmallClass("key1", 1),
SmallClass("key2", 2),
SmallClass("key3", 3),
SmallClass("key4", 3),
SmallClass("er", 9))

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

Например, для получения чисел для каждого диапазона 10 (0 – 9, 10 – 19, 20-29, и т.д.), мы можем округлить в меньшую сторону до ближайших 10, и это значение будет задано нашим селектором.:

val actual = array.distinctBy { Math.floor(it.num / 10.0) }

7. Chunked

Одной из интересных функций Kotlin 1.2 является разбиение на фрагменты. При разбиении на фрагменты берется единственная повторяемая коллекция и создается новый список фрагментов, соответствующих заданному размеру. Это не работает с массивами, только с повторяемыми объектами.

Мы можем разбивать на фрагменты, просто задавая размер фрагмента для извлечения:

@Test
fun givenDNAFragmentString_whenChunking_thenProduceListOfChunks() {
val dnaFragment = "ATTCGCGGCCGCCAA"

val fragments = dnaFragment.chunked(3)

assertIterableEquals(listOf("ATT", "CGC", "GGC", "CGC", "CAA"), fragments)
}

Или размер и трансформатор:

@Test
fun givenDNAString_whenChunkingWithTransformer_thenProduceTransformedList() {
val codonTable = mapOf(
"ATT" to "Isoleucine",
"CAA" to "Glutamine",
"CGC" to "Arginine",
"GGC" to "Glycine")
val dnaFragment = "ATTCGCGGCCGCCAA"

val proteins = dnaFragment.chunked(3) { codon ->
codonTable[codon.toString()] ?: error("Unknown codon")
}

assertIterableEquals(listOf(
"Isoleucine", "Arginine",
"Glycine", "Arginine", "Glutamine"), proteins)
}

Приведенный выше пример выбора фрагментов ДНК взят из документации Kotlin по chunked, доступной здесь.

При передаче chunked размер, который не является делителем размера нашей коллекции. В таких случаях последним элементом в нашем списке фрагментов будет просто список меньшего размера.

Будьте осторожны, чтобы не предположить, что каждый фрагмент имеет полный размер, и не столкнуться с исключением ArrayIndexOutOfBoundsException!

8. Заключение

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