Объяснение Kotlin groupBy() и partition()

В этой статье мы рассмотрим две полезные функции в Kotlin — GroupBy() и Partition(). Мы увидим, как их можно применить в нашем коде и в каких случаях они могут помочь.

Подготовка кода

Но прежде чем мы приступим к рассмотрению, давайте реализуем необходимый код для работы.

Добавим класс Item.kt:

data class Item(
 val name: String,
 val type: ItemType
)

enum class ItemType {
 STANDARD, PREMIUM, OTHER
}

Далее давайте реализуем простой список, заполненный экземплярами Item:

val items = listOf(
 Item(name = "Item #1", type = ItemType.STANDARD),
 Item(name = "Item #2", type = ItemType.OTHER),
 Item(name = "Item #3", type = ItemType.PREMIUM),
 Item(name = "Item #4", type = ItemType.STANDARD),
 Item(name = "Item #5", type = ItemType.PREMIUM)
)

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

groupBy()

После всего этого давайте начнем с первой функции сегодняшнего сравнения Kotlin groupBy() и Partition().

Как следует из названия, эта функция позволяет нам группировать элементы массива по ключу, возвращаемому keySelector (который нам нужно передать). В результате он возвращает карту, где каждому ключу сопоставлена ​​карта значений из сопоставленного массива.

Простая группаBy()

Чтобы лучше понять это, давайте посмотрим на первый пример:

val groupedByItems = items.groupBy { it.type }
println(groupedByItems)

Если мы запустим его, мы должны увидеть следующий вывод (отформатированный вручную для удобства чтения):

{
 STANDARD=[Item(name=Item #1, type=STANDARD), Item(name=Item #4, type=STANDARD)], 
 OTHER=[Item(name=Item #2, type=OTHER)], 
 PREMIUM=[Item(name=Item #3, type=PREMIUM), Item(name=Item #5, type=PREMIUM)]
}

Мы ясно видим, что наш список был преобразован в Map<ItemType, List<Item>>.

Кроме того, мы можем использовать ссылки на свойства Kotlin , чтобы сделать наш код еще более понятным:

val groupedByItemsReference = items.groupBy(Item::type)
println(groupedByItemsReference)

И результат остается точно таким же:

{
 STANDARD=[Item(name=Item #1, type=STANDARD), Item(name=Item #4, type=STANDARD)], 
 OTHER=[Item(name=Item #2, type=OTHER)], 
 PREMIUM=[Item(name=Item #3, type=PREMIUM), Item(name=Item #5, type=PREMIUM)]
}

Преобразование значений

В качестве следующего примера представим, что нам нужно получить списки имен для каждого типа.

Для этого мы можем использовать другую версию функции groupBy() :

val groupedByItemsNamesOnly = items.groupBy(
 (Item::type),
 (Item::name)
)
println(groupedByItemsNamesOnly)

Далее давайте проверим наш пример:

{
 STANDARD=[Item #1, Item #4], 
 OTHER=[Item #2], 
 PREMIUM=[Item #3, Item #5]
}

Мы ясно видим, что на этот раз возвращается Map<ItemType, List<String>>.

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

Разделить список

Напротив, давайте посмотрим на пример, объясняющий, как не следует использовать groupBy() .

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

  • первый - содержит только PREMIUM items
  • а второй - с другими внутри

Давайте посмотрим, как эту задачу можно выполнить с помощью groupBy():

val premiumItems = items.groupBy(Item::type)[ItemType.PREMIUM]
val otherItems = 
 items.groupBy(Item::type)
 .filterNot { it.key == ItemType.PREMIUM }
 .values
 .flatten()

println("Premium: $premiumItems")
println("Other types: $otherItems")

В результате мы должны получить следующий вывод:

Premium: [Item(name=Item #3, type=PREMIUM), Item(name=Item #5, type=PREMIUM)]
Other types: [Item(name=Item #1, type=STANDARD), Item(name=Item #4, type=STANDARD), Item(name=Item #2, type=OTHER)]

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

Конечно, его можно было бы просто привести к такому виду:

val premiumTypesFiltered = items.filter { it.type == ItemType.PREMIUM }
val otherTypesFiltered = items.filterNot { it.type == ItemType.PREMIUM }

println("Premium: $premiumTypesFiltered")
println("Other types: $otherTypesFiltered")

Тем не менее, в следующей главе мы увидим, что можем сделать этот код еще лучше.

partition()

С учетом вышесказанного, давайте посмотрим, что такое функция partition() в Котлине . Согласно документации:

Разбивает исходный массив на пару списков, где первый список содержит элементы, для которых предикат выдал true, а второй список содержит элементы, для которых предикат выдал false.

Проще говоря, от нас требуется передать предикат (логическое выражение) и в результате мы получаем Пара, содержащая два списка:

  • первый, с элементами, оцененными как true
  • и второй - то же самое, но с false

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

public inline fun <T> Iterable<T>.partition(
  predicate: (T) -> Boolean
): Pair<List<T>, List<T>> {
  val first = ArrayList<T>()
  val second = ArrayList<T>()
    for (element in this) {
      if (predicate(element)) {
        first.add(element)
      } else {
        second.add(element)
      }
    }
  return Pair(first, second)
}

Мы ясно видим, что элементы, получившие значение true, добавляются в первый ArrayList, тогда как элементы, получившие значение false, попадают во второй . Наконец, оба они объединяются в новый экземпляр Pair.

Simple partition()

Учитывая все вышесказанное, давайте посмотрим, чем нам может помочь partition().

Повторим пример разделения с уже имеющимися знаниями:

val partitioned = items.partition { it.type == ItemType.PREMIUM }

println("Premium: ${partitioned.first}")
println("Other types: ${partitioned.second}")

Далее давайте запустим приведенный выше код:

Premium: [Item(name=Item #3, type=PREMIUM), Item(name=Item #5, type=PREMIUM)]
Other types: [Item(name=Item #1, type=STANDARD), Item(name=Item #2, type=OTHER), Item(name=Item #4, type=STANDARD)]

Как мы видим, благодаря partition() мы смогли значительно сократить объём кода, чтобы добиться того же результата.

partition() и объявления деструктуризации

Тем не менее, давайте посмотрим, как мы можем еще больше улучшить наш код, объединив partition() с объявлениями деструктуризации.

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

Давайте проверим приведенный ниже код, чтобы увидеть его на практике:

val (premiumTypes, otherTypes) = items.partition { it.type == ItemType.PREMIUM }

println("Premium: $premiumTypes")
println("Other types: $otherTypes")

Аналогично, давайте запустим пример:

Premium: [Item(name=Item #3, type=PREMIUM), Item(name=Item #5, type=PREMIUM)]
Other types: [Item(name=Item #1, type=STANDARD), Item(name=Item #2, type=OTHER), Item(name=Item #4, type=STANDARD)]

Как мы видим, благодаря комбинации partition() и объявлений деструктуризации мы создали две новые переменные: premiumTypes иotherTypes.

По сравнению с предыдущим примером это эквивалентно:

val premiumTypes = partitioned.first
val otherTypes = partitioned.second

5. Kotlin groupBy() и partition() Резюме

И это все, что касается этой статьи, посвященной функциям Kotlin groupBy() и Partition(). Спасибо за прочтение.