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

Фильтрация — одна из самых популярных задач при работе с коллекцией. В Kotlin условия фильтра задаются с помощью предикатов — лямбда-функций, которые принимают элемент коллекции, а возвращают логическое значение (boolean): true означает, что элемент соответствует предикату, false - не соответствует.

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

Фильтр с предикатом

Основная функция для фильтра коллекций - filter(). Если использовать filter() с предикатом, то будут возвращены те элементы, которые ему соответствуют. Для List и Set функция возвращает результат типа List, для Map возвращается Map.

fun main() {
    val numbers = listOf("one", "two", "three", "four")  
    val longerThan3 = numbers.filter { it.length > 3 }
    println(longerThan3) // [three, four]

    val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
    val filteredMap = numbersMap.filter { (key, value) -> key.endsWith("1") && value > 10}
    println(filteredMap) // {key11=11}
}

В функции filter() предикаты могут проверять только значения элементов. Если при фильтрации вы хотите использовать индексы элементов, то вам нужна функция filterIndexed(). Она принимает предикат с двумя аргументами: индекс и значение элемента.

Если вы хотите отфильтровать элементы, которые не соответствуют заданному условию, то используйте функцию filterNot(). Она возвращает те элементы, для которых предикат вернул значение false.

fun main() {
    val numbers = listOf("one", "two", "three", "four")

    val filteredIdx = numbers.filterIndexed { index, s -> (index != 0) && (s.length < 5)  }
    val filteredNot = numbers.filterNot { it.length <= 3 }

    println(filteredIdx) // [two, four]
    println(filteredNot) // [three, four]
}

Также существуют функции, которые фильтруют только элементы заданного типа:

  • filterIsInstance() возвращает элементы заданного типа. При вызове функции filterIsInstance<T>() для списка типа List<Any> вернётся список типа List<T>, что позволяет вызывать специфичные для элементов T функции.

    fun main() {
      val numbers = listOf(null, 1, "two", 3.0, "four")
      println("All String elements in upper case:")
      numbers.filterIsInstance<String>().forEach {
          println(it.uppercase())
      }
    }
    
    // В логе будет:
    // All String elements in upper case:
    // TWO
    // FOUR
    
  • filterNotNull() возвращает значения неравные null. При вызове функции для списка List<T?> вернётся список типа List<T: Any>, что позволяет обращаться к элементам как к ненулевым объектам.

    fun main() {
        val numbers = listOf(null, "one", "two", null)
        numbers.filterNotNull().forEach {
            println(it.length)   // length is unavailable for nullable Strings
        }
    }
    
    // В логе будет:
    // 3
    // 3
    

Фильтр с разделением

Другая функция для фильтрации - partition() - фильтрует коллекцию по предикату и сохраняет элементы, которые ему не соответствуют, в отдельном списке. Возвращает объект типа Pair<List, List>, где первый список содержит элементы, соответствующие предикату, а второй — все остальные элементы из исходной коллекции.

fun main() {
    val numbers = listOf("one", "two", "three", "four")
    val (match, rest) = numbers.partition { it.length > 3 }

    println(match) // [three, four]
    println(rest) // [one, two]
}

Проверочные предикаты

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

  • any() - возвращает true, если хотя бы один элемент соответствует предикату.
  • none() - возвращает true, если ни один элемент не соответствует предикату.
  • all() - возвращает true, если все элементы соответствуют предикату. Обратите внимание, что если вызвать all() для пустой коллекции с любым валидным предикатом, то функция вернёт true. Такое поведение известно как бессмысленная истина (ориг. vacuous truth).
fun main() {
    val numbers = listOf("one", "two", "three", "four")

    println(numbers.any { it.endsWith("e") }) // true
    println(numbers.none { it.endsWith("a") }) // true
    println(numbers.all { it.endsWith("e") }) // false

    println(emptyList<Int>().all { it > 5 })   // true - бессмысленная истина
}

any() и none() могут использоваться без предиката. В этом случае они просто проверяют пуста ли коллекция. any() возвращает true, если в коллекции есть элементы, а none() наоборот, возвращает true, если коллекция пуста.

fun main() {
    val numbers = listOf("one", "two", "three", "four")
    val empty = emptyList<String>()

    println(numbers.any()) // true
    println(empty.any()) // false

    println(numbers.none()) // false
    println(empty.none()) // true
}