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

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

1. обзор

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

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

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

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

2. Dropс

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

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

    assertIterableEquals(expected, result)
}

С другой стороны,if we want to drop the last n elements, мы вызываемdropLast:

@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)
}

dropLastWhile удалил последние три1f из списка, поскольку метод перебирал каждый элемент до первого экземпляра, в котором элемент массива не был равен1f.

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

dropWhile - это еще один фильтр, который принимает предикат, ноdropWhile работает с индексом0 → n, аdropLastWhile работает с индексомn → 0.

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

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 сохраняет элементы.

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

An important note here заключается в том, чтоtakeIf НЕ является методом сбора. takeIf использует предикат, чтобы определить, возвращать ли значениеnull или нет - подумайтеOptional#filter.

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

4. Filterс

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

@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 }
}

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

This allows us to take several collections and filter them into a single, accumulative collection.

Этот пример берет; массив, последовательность и список.

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

@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()

    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)
}

Фильтры с предикатом или без него также хорошо работают сMaps:

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

Очень полезная пара методов фильтрации - этоfilterNotNull иfilterNotNullTo, которые будутjust filter out all null elements.

Наконец, если нам когда-нибудь понадобится использовать индекс элемента коллекции,filterIndexed and filterIndexedTo provide the ability to use a predicate lambda with both the element and its position index.

5. Sliceс

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

@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)
}

Срез может идти вверх или вниз.

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

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

Однако выход за пределы коллекции с использованиемa Range with steps может вызватьArrayIndexOutOfBoundsException:

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

6. отчетливый

Другой фильтр, который мы рассмотрим в этой статье, отличается. We can use this method to collect unique objects from our list:

@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)
}

У нас также есть возможность использовать функцию выбора. The selector returns the value we’re going to evaluate for uniqueness.с

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

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

используя массивSmallClass:

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

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

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

Функция не обязана напрямую возвращать свойство переменнойwe can also perform calculations to determine our distinct values.

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

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

7. Блочная

Одна интересная особенность Kotlin 1.2 -chunked. При разбиении на части берется одна коллекцияIterable и создается новыйList фрагментов, соответствующих определенному размеру. This doesn’t work with Arrays; only Iterables.

Мы можем разделить фрагмент на любой размер:

@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 по доступным фрагментамhere.

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

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

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

Все фильтры Kotlin позволяют нам применять лямбда-выражения, чтобы определить, должен ли элемент фильтроваться или нет. Not all of these functions can be used on Maps, однако все функции фильтрации, которые работают сMaps, будут работать сArrays.

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

Как всегда доступны все примерыover on GitHub.