🎯 Kotlin

Methoden für Arrays und Sammlungen in Kotlin: Eine ausführliche Anleitung

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты — библиотекарь. У тебя есть стопка книг (это наш массив или коллекция). Каждый день тебе нужно выполнять разные операции с этими книгами:

  • Найти книгу по названию (поиск)
  • Отсортировать книги по алфавиту (сортировка)
  • Отобрать только те книги, которые толще 300 страниц (фильтрация)
  • Пересчитать, сколько книг написал каждый автор (группировка)
  • Проверить, есть ли хотя бы одна книга с картинками (проверка условия)

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

💡 Важно: Методы коллекций — это как набор швейцарского ножа. Вместо того чтобы писать 10 строк кода для поиска элемента, ты пишешь одну строку с методом .find(). Это экономит время и уменьшает количество ошибок.

В Kotlin коллекции делятся на два типа: изменяемые (mutable) и неизменяемые (immutable). Неизменяемые — как витрина музея: ты можешь смотреть, но не можешь трогать. Изменяемые — как твоя личная полка: можешь добавлять, убирать и переставлять книги.

Теория: как это работает изнутри

Давай заглянем под капот. Когда ты создаёшь коллекцию в Kotlin, происходит вот что:

  1. Выделение памяти — компьютер резервирует место для хранения элементов
  2. Инициализация — элементы размещаются в памяти последовательно (для списков) или с хеш-кодами (для множеств и словарей)
  3. Создание ссылки — переменная получает адрес первого элемента

Основные типы коллекций в Kotlin:

Тип Описание Особенности
List Упорядоченный список (как очередь в магазине) Есть индексы, можно повторять элементы
Set Множество уникальных элементов (как набор марок) Нет повторов, порядок не гарантирован
Map Словарь "ключ-значение" (как телефонная книга) Быстрый поиск по ключу
Array Фиксированный массив (как поднос с ячейками) Размер задаётся при создании

Когда ты вызываешь метод коллекции, например .filter(), Kotlin внутри делает следующее:

  1. Создаёт новую пустую коллекцию
  2. Проходит по каждому элементу исходной коллекции
  3. Проверяет условие (предикат) для каждого элемента
  4. Если условие верно — добавляет элемент в новую коллекцию
  5. Возвращает новую коллекцию
⚡ Важно: Большинство методов коллекций в Kotlin — функции высшего порядка. Это значит, что они принимают другую функцию (лямбду) в качестве параметра. Лямбда — это блок кода, который ты передаёшь методу, чтобы сказать "сделай это с каждым элементом".

Синтаксис в Kotlin: все формы и варианты

Давай разберём основные методы. Я сгруппировал их по назначению, чтобы было легче запомнить.

1. Создание коллекций

// Неизменяемые коллекции (только для чтения)
val numbers = listOf(1, 2, 3, 4, 5)           // List
val words = setOf("кот", "собака", "кот")     // Set — "кот" будет только один
val map = mapOf(1 to "один", 2 to "два")      // Map

// Изменяемые коллекции
val mutableNumbers = mutableListOf(1, 2, 3)   // MutableList
val mutableWords = mutableSetOf("а", "б")     // MutableSet
val mutableMap = mutableMapOf(1 to "раз")     // MutableMap

// Пустые коллекции
val emptyList = emptyList<String>()           // Пустой список
val emptySet = emptySet<Int>()                // Пустое множество

2. Основные методы List и MutableList

Метод Что делает Пример
size Возвращает количество элементов list.size
get(index) Получает элемент по индексу list[2]
add(element) Добавляет элемент (только для MutableList) mutableList.add(5)
remove(element) Удаляет первый найденный элемент mutableList.remove(3)
contains(element) Проверяет, есть ли элемент list.contains(2)
indexOf(element) Возвращает индекс первого вхождения list.indexOf(4)

3. Функциональные методы (самые мощные!)

Метод Что делает Результат
filter Оставляет только те элементы, которые проходят проверку Новая коллекция
map Преобразует каждый элемент по заданному правилу Новая коллекция
forEach Выполняет действие для каждого элемента Ничего (Unit)
find Находит первый элемент, подходящий под условие Один элемент или null
any Проверяет, есть ли хотя бы один подходящий элемент Boolean
all Проверяет, все ли элементы подходят Boolean
sorted Сортирует коллекцию Новая отсортированная коллекция
groupBy Группирует элементы по ключу Map
reduce Последовательно сворачивает коллекцию в одно значение Одно значение

4. Специальные методы для Map

val map = mapOf("a" to 1, "b" to 2, "c" to 3)

// Получение значения по ключу
println(map["a"])           // 1
println(map.getOrDefault("d", 0))  // 0 (если ключа нет)

// Проверка наличия
println(map.containsKey("a"))  // true
println(map.containsValue(2))  // true

// Получение всех ключей и значений
println(map.keys)    // [a, b, c]
println(map.values)  // [1, 2, 3]

Примеры кода — от простого к сложному

Пример 1: Базовые операции со списком

fun main() {
    // Создаём изменяемый список чисел
    val numbers = mutableListOf(10, 20, 30, 40, 50)
    
    // Добавляем элемент в конец
    numbers.add(60)
    println("После добавления: $numbers")  // [10, 20, 30, 40, 50, 60]
    
    // Удаляем элемент по значению
    numbers.remove(30)
    println("После удаления 30: $numbers")  // [10, 20, 40, 50, 60]
    
    // Получаем элемент по индексу
    val firstElement = numbers[0]
    println("Первый элемент: $firstElement")  // 10
    
    // Проверяем, есть ли элемент в списке
    val hasForty = numbers.contains(40)
    println("Есть ли 40? $hasForty")  // true
    
    // Узнаём размер списка
    println("Размер списка: ${numbers.size}")  // 5
}

Пример 2: Фильтрация списка

fun main() {
    val scores = listOf(45, 78, 92, 33, 67, 88, 51)
    
    // Фильтруем: оставляем только те, что больше или равны 60 (проходной балл)
    val passed = scores.filter { it >= 60 }
    println("Сдали экзамен: $passed")  // [78, 92, 67, 88]
    
    // Фильтруем с явным именем переменной
    val failed = scores.filter { score -> score < 60 }
    println("Не сдали: $failed")  // [45, 33, 51]
    
    // Можно комбинировать условия
    val excellent = scores.filter { it >= 85 && it <= 100 }
    println("Отличники: $excellent")  // [92, 88]
}

Пример 3: Преобразование элементов с map

fun main() {
    val names = listOf("анна", "пётр", "мария", "иван")
    
    // Преобразуем: делаем первую букву заглавной
    val capitalized = names.map { name ->
        name.replaceFirstChar { it.uppercase() }
    }
    println("С заглавной буквы: $capitalized")  // [Анна, Пётр, Мария, Иван]
    
    // Преобразуем: считаем длину каждого имени
    val nameLengths = names.map { it.length }
    println("Длины имён: $nameLengths")  // [4, 4, 5, 4]
    
    // Комбинируем filter и map
    val longNames = names
        .filter { it.length > 4 }
        .map { it.uppercase() }
    println("Длинные имена (заглавные): $longNames")  // [МАРИЯ]
}

Пример 4: Поиск элементов

fun main() {
    val products = listOf(
        "Молоко" to 85,
        "Хлеб" to 45,
        "Сыр" to 250,
        "Яблоки" to 120
    )
    
    // Находим первый продукт дороже 100 рублей
    val expensiveProduct = products.find { it.second > 100 }
    println("Первый дорогой продукт: $expensiveProduct")  // (Сыр, 250)
    
    // Проверяем, есть ли продукты дешевле 50 рублей
    val hasCheap = products.any { it.second < 50 }
    println("Есть дешёвые продукты? $hasCheap")  // true (Хлеб за 45)
    
    // Проверяем, все ли продукты дороже 30 рублей
    val allExpensive = products.all { it.second > 30 }
    println("Все дороже 30? $allExpensive")  // true
    
    // Получаем индекс первого подходящего элемента
    val index = products.indexOfFirst { it.first == "Сыр" }
    println("Индекс сыра: $index")  // 2
}

Пример 5: Сортировка

fun main() {
    val numbers = listOf(5, 2, 8, 1, 9, 3)
    
    // Сортировка по возрастанию
    val sortedAsc = numbers.sorted()
    println("По возрастанию: $sortedAsc")  // [1, 2, 3, 5, 8, 9]
    
    // Сортировка по убыванию
    val sortedDesc = numbers.sortedDescending()
    println("По убыванию: $sortedDesc")  // [9, 8, 5, 3, 2, 1]
    
    // Сортировка объектов по свойству
    data class Person(val name: String, val age: Int)
    val people = listOf(
        Person("Анна", 25),
        Person("Пётр", 30),
        Person("Мария", 22)
    )
    
    // Сортируем людей по возрасту
    val sortedByAge = people.sortedBy { it.age }
    println("По возрасту: $sortedByAge")  // [Мария(22), Анна(25), Пётр(30)]
    
    // Сортируем по убыванию возраста
    val sortedByAgeDesc = people.sortedByDescending { it.age }
    println("По убыванию возраста: $sortedByAgeDesc")  // [Пётр(30), Анна(25), Мария(22)]
}

Пример 6: Группировка и агрегация

fun main() {
    data class Student(val name: String, val grade: Int, val subject: String)
    
    val students = listOf(
        Student("Анна", 5, "Математика"),
        Student("Пётр", 4, "Физика"),
        Student("Мария", 5, "Математика"),
        Student("Иван", 3, "Физика"),
        Student("Ольга", 4, "Математика")
    )
    
    // Группируем студентов по предмету
    val bySubject = students.groupBy { it.subject }
    println("По предметам:")
    bySubject.forEach { (subject, group) ->
        println("  $subject: ${group.map { it.name }}")
    }
    // Вывод:
    //   Математика: [Анна, Мария, Ольга]
    //   Физика: [Пётр, Иван]
    
    // Считаем количество студентов по оценкам
    val countByGrade = students.groupingBy { it.grade }.eachCount()
    println("По оценкам: $countByGrade")  // {5=2, 4=2, 3=1}
    
    // Находим максимальную оценку
    val maxGrade = students.maxOf { it.grade }
    println("Максимальная оценка: $maxGrade")  // 5
    
    // Суммируем все оценки
    val totalGrades = students.sumOf { it.grade }
    println("Сумма оценок: $totalGrades")  // 21
}

Пример 7: Работа с Map (словарями)

fun main() {
    // Создаём телефонную книгу
    val phoneBook = mutableMapOf(
        "Анна" to "+7-123-456-78-90",
        "Пётр" to "+7-098-765-43-21",
        "Мария" to "+7-555-123-45-67"
    )
    
    // Добавляем новый контакт
    phoneBook["Иван"] = "+7-111-222-33-44"
    println("После добавления: $phoneBook")
    
    // Обновляем существующий контакт
    phoneBook["Анна"] = "+7-999-888-77-66"
    println("После обновления Анны: $phoneBook")
    
    // Получаем номер по имени (безопасно)
    val peterPhone = phoneBook["Пётр"] ?: "Номер не найден"
    println("Телефон Петра: $peterPhone")
    
    // Удаляем контакт
    phoneBook.remove("Мария")
    println("После удаления Марии: $phoneBook")
    
    // Проходим по всем записям
    println("Все контакты:")
    phoneBook.forEach { (name, phone) ->
        println("  $name: $phone")
    }
    
    // Фильтруем контакты (например, только те, что начинаются с +7-5)
    val filtered = phoneBook.filter { (_, phone) ->
        phone.startsWith("+7-5")
    }
    println("Контакты с номерами на +7-5: $filtered")
}

Пример 8: Сложные цепочки методов

fun main() {
    data class Order(val id: Int, val amount: Double, val isPaid: Boolean)
    
    val orders = listOf(
        Order(1, 1500.0, true),
        Order(2, 2300.0, false),
        Order(3, 800.0, true),
        Order(4, 3200.0, true),
        Order(5, 1200.0, false)
    )
    
    // Задача: найти среднюю сумму оплаченных заказов
    val averagePaid = orders
        .filter { it.isPaid }                    // Оставляем только оплаченные
        .map { it.amount }                       // Берём только суммы
        .let { paidAmounts ->                    // let позволяет работать с результатом
            if (paidAmounts.isEmpty()) 0.0       // Если нет оплаченных, возвращаем 0
            else paidAmounts.average()           // Иначе считаем среднее
        }
    
    println("Средняя сумма оплаченных заказов: $averagePaid")  // 1833.33...
    
    // Задача: получить список ID неоплаченных заказов, отсортированный по убыванию суммы
    val unpaidIds = orders
        .filter { !it.isPaid }
        .sortedByDescending { it.amount }
        .map { it.id }
    
    println("ID неоплаченных заказов (по убыванию суммы): $unpaidIds")  // [2, 5]
}

Сравнение с Python: чем похоже и чем отличается

Если ты уже знаком с Python, тебе будет проще. Давай сравним основные моменты:

Операция Kotlin Python
Создание списка listOf(1, 2, 3) [1, 2, 3]
Фильтрация list.filter { it > 5 } [x for x in list if x > 5]
Преобразование list.map { it * 2 } [x * 2 for x in list]
Сортировка list.sorted() sorted(list)
Группировка list.groupBy { it.key } itertools.groupby (сложнее)
Проверка наличия list.contains(5) 5 in list
Словарь mapOf("a" to 1) {"a": 1}

Главные отличия:

  1. Типизация: Kotlin строго типизирован. Ты не сможешь положить строку в список чисел. Python — динамический, можно смешивать типы.
  2. Неизменяемость: В Kotlin коллекции по умолчанию неизменяемые. В Python списки всегда изменяемые.
  3. Синтаксис лямбд: В Kotlin лямбды пишутся в фигурных скобках { it -> ... }. В Python — lambda x: ....
  4. Null-безопасность: В Kotlin методы могут возвращать null (например, find), и компилятор заставит тебя это обработать. В Python — просто исключение.
  5. Цепочки методов: В Kotlin цепочки читаются сверху вниз (как в примере 8). В Python чаще используют вложенные вызовы.
💡 Совет: Если ты переходишь с Python на Kotlin, запомни главное правило: в Kotlin почти всё — выражение. Даже if может возвращать значение. Это делает код более компактным и функциональным.

Типичные ошибки и подводные камни

Ошибка 1: Изменение неизменяемой коллекции

fun main() {
    val list = listOf(1, 2, 3)
    // list.add(4)  // ОШИБКА! listOf создаёт неизменяемый список
    
    // Правильно: использовать mutableListOf
    val mutableList = mutableListOf(1, 2, 3)
    mutableList.add(4)  // Работает!
}

Ошибка 2: Забыли обработать null

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    
    // find возвращает null, если элемент не найден
    val found = numbers.find { it > 10 }
    // println(found + 5)  // ОШИБКА! found может быть null
    
    // Правильно: использовать безопасный вызов или элвис-оператор
    println(found?.plus(5) ?: "Элемент не найден")
}

Ошибка 3: Путаница между filter и map

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    
    // filter ОСТАВЛЯЕТ элементы, проходящие условие
    val filtered = numbers.filter { it > 3 }
    println("filter: $filtered")  // [4, 5]
    
    // map ПРЕОБРАЗУЕТ каждый элемент
    val mapped = numbers.map { it * 2 }
    println("map: $mapped")  // [2, 4, 6, 8, 10]
    
    // Частая ошибка: пытаются изменить элемент внутри filter
    // numbers.filter { it * 2 }  // ОШИБКА! filter ждёт Boolean
}

Ошибка 4: Изменение коллекции во время итерации

fun main() {
    val list = mutableListOf(1, 2, 3, 4, 5)
    
    // ОПАСНО! Изменение списка во время обхода
    // for (item in list) {
    //     if (item % 2 == 0) list.remove(item)  // ConcurrentModificationException!
    // }
    
    // Правильно: использовать итератор или filter
    val evenNumbers = list.filter { it % 2 == 0 }
    println("Чётные: $evenNumbers")  // [2, 4]
    
    // Или удалять через итератор
    val iterator = list.iterator()
    while (iterator.hasNext()) {
        if (iterator.next() % 2 == 0) iterator.remove()
    }
    println("После удаления чётных: $list")  // [1, 3, 5]
}

Ошибка 5: Путаница между == и ===

fun main() {
    val list1 = listOf(1, 2, 3)
    val list2 = listOf(1, 2, 3)
    
    // == сравнивает содержимое
    println(list1 == list2)   // true (содержимое одинаковое)
    
    // === сравнивает ссылки
    println(list1 === list2)  // false (разные объекты в памяти)
}

Ошибка 6: Неправильное использование groupBy

fun main() {
    data class Person(val name: String, val age: Int)
    val people = listOf(
        Person("Анна", 25),
        Person("Пётр", 30),
        Person("Мария", 25)
    )
    
    // groupBy возвращает Map, где ключ — результат лямбды
    val byAge = people.groupBy { it.age }
    println(byAge)  // {25=[Анна(25), Мария(25)], 30=[Пётр(30)]}
    
    // Если нужна просто группировка по свойству, используй groupingBy
    val countByAge = people.groupingBy { it.age }.eachCount()
    println(countByAge)  // {25=2, 30=1}
}

Лучшие практики и советы профессионалов

1. Используй неизменяемые коллекции по умолчанию

Всегда начинай с listOf, setOf, mapOf. Переходи на mutableListOf только когда точно нужно изменять коллекцию. Это делает код безопаснее и предсказуемее.

2. Предпочитай функциональные методы циклам

Вместо:

val result = mutableListOf<Int>()
for (item in list) {
    if (item > 5) {
        result.add(item * 2)
    }
}

Пиши:

val result = list.filter { it > 5 }.map { it * 2 }

Это короче, понятнее и менее подвержено ошибкам.

3. Используй цепочки методов, но не переусердствуй

Цепочки из 3-4 методов — это хорошо. Цепочка из 10 методов — уже сложно читать. Если цепочка становится длинной, разбей её на несколько шагов с понятными названиями переменных.

4. Помни про производительность

Некоторые методы создают новые коллекции. Если работаешь с большими данными (миллионы элементов), это может быть дорого. В таких случаях:

  • Используй asSequence() для ленивых вычислений
  • Избегай многократных проходов по коллекции
  • Для простых проверок используй any, all, none вместо фильтрации

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

val pairs = listOf(1 to "один", 2 to "два", 3 to "три")

// Вместо:
pairs.forEach { pair -> println("${pair.first}: ${pair.second}") }

// Пиши:
pairs.forEach { (key, value) -> println("$key: $value") }

6. Не бойся let, apply, also, run

Эти функции-расширения помогают писать более чистый код:

// let — выполняет код с объектом как параметром
val length = "Привет".let { 
    println("Обрабатываю строку: $it")
    it.length 
}

// apply — изменяет объект и возвращает его
val list = mutableListOf<Int>().apply {
    add(1)
    add(2)
    add(3)
}

// also — выполняет побочное действие
val numbers = listOf(1, 2, 3).also {
    println("Исходный список: $it")
}.filter { it > 1 }

7. Используй typealias для сложных типов

// Вместо:
fun processData(data: Map<String, List<Pair<Int, String>>>) {}

// Используй:
typealias UserData = Map<String, List<Pair<Int, String>>>
fun processData(data: UserData) {}

Вопросы для самопроверки

  1. Вопрос: В чём разница между listOf и mutableListOf?

    Ответ: listOf создаёт неизменяемый список (нельзя добавлять/удалять элементы), mutableListOf — изменяемый. Неизменяемые коллекции безопаснее в многопоточном коде.

  2. Вопрос: Что вернёт listOf(1, 2, 3).filter { it > 5 }?