1. 概述

Kotlin 集合是功能强大的数据结构,相较于 Java 集合提供了更多实用的方法。本文将介绍 Kotlin 集合中常用的几种过滤操作方法,掌握这些方法后,读者可以轻松理解并使用未在本文中明确列出的其他相关方法。

📌 所有这些方法都不会修改原始集合,而是返回一个新的集合。

我们会用到 Kotlin 的 lambda 表达式来实现过滤逻辑,如果你对 lambda 不太熟悉,可以参考我们的 Kotlin Lambda 文章。

2. drop 操作

drop 是一种简单的方式来裁剪集合。它会从集合中移除指定数量的元素,并返回一个新集合:

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

    assertIterableEquals(expected, result)
}

如果你想要移除最后几个元素,可以使用 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)
}

还可以使用 dropLastWhile,它会从尾部开始遍历,直到遇到不符合条件的元素为止:

@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 会从末尾开始删除元素,直到遇到第一个不满足条件的元素为止。

📌 dropWhiledropLastWhile 类似,但方向相反,它从索引 0 开始删除。

⚠️ 如果你尝试删除的元素数大于集合本身长度,结果将是一个空列表。

3. take 操作

takedrop 类似,但它保留指定数量的元素:

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

📌 take 保留符合条件的元素;drop 删除符合条件的元素。

⚠️ takeIf 并不是集合操作方法,它是对单个对象的操作,用于判断是否返回对象本身或 null,类似于 Java 的 Optional#filter

要根据条件筛选出所有匹配的元素,应该使用 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 }
}

如果你想将多个集合的过滤结果合并到一个可变集合中,可以使用 filterTo

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

📌 filterNotNullfilterNotNullTo 可以方便地移除所有 null 元素。

📌 如果你需要使用索引进行过滤,可以使用 filterIndexedfilterIndexedTo

5. slice 操作

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

📌 范围可以是升序也可以是降序。

📌 使用 range 时还可以指定步长(step)。

⚠️ 如果你使用带步长的范围超出集合边界,会抛出 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)
}

还可以使用 distinctBy,它接受一个选择器函数来决定唯一性的判断依据:

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

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

📌 选择器函数不一定要直接返回对象属性,也可以是计算值,例如根据数值范围进行去重:

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

7. chunked 操作

Kotlin 1.2 引入了 chunked 方法,用于将一个 Iterable 切分成多个子块:

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

📌 如果集合长度不能被 chunk size 整除,最后一个 chunk 会比其他 chunk 小。

⚠️ 不要假设每个 chunk 都是满的,否则可能遇到 ArrayIndexOutOfBoundsException

更多关于 chunked 的示例请参考 Kotlin 官方文档:chunked

8. 总结

Kotlin 集合提供了丰富的过滤方法,它们都支持使用 lambda 表达式进行条件筛选。虽然不是所有方法都能用于 Map,但适用于 Map 的方法也都能用于 Array

📌 建议查阅 Kotlin 官方文档 了解每个方法支持的数据结构。

📌 所有示例代码都可以在 GitHub 仓库 中找到。


原始标题:Filtering Kotlin Collections