Methoden für Arrays und Sammlungen in Kotlin: Eine ausführliche Anleitung
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты — библиотекарь. У тебя есть стопка книг (это наш массив или коллекция). Каждый день тебе нужно выполнять разные операции с этими книгами:
- Найти книгу по названию (поиск)
- Отсортировать книги по алфавиту (сортировка)
- Отобрать только те книги, которые толще 300 страниц (фильтрация)
- Пересчитать, сколько книг написал каждый автор (группировка)
- Проверить, есть ли хотя бы одна книга с картинками (проверка условия)
В программировании мы постоянно работаем с наборами данных: список пользователей, корзина покупок, результаты поиска, сообщения в чате. Методы массивов и коллекций — это готовые инструменты, которые позволяют делать всё это быстро, красиво и без лишнего кода.
💡 Важно: Методы коллекций — это как набор швейцарского ножа. Вместо того чтобы писать 10 строк кода для поиска элемента, ты пишешь одну строку с методом .find(). Это экономит время и уменьшает количество ошибок.
В Kotlin коллекции делятся на два типа: изменяемые (mutable) и неизменяемые (immutable). Неизменяемые — как витрина музея: ты можешь смотреть, но не можешь трогать. Изменяемые — как твоя личная полка: можешь добавлять, убирать и переставлять книги.
Теория: как это работает изнутри
Давай заглянем под капот. Когда ты создаёшь коллекцию в Kotlin, происходит вот что:
- Выделение памяти — компьютер резервирует место для хранения элементов
- Инициализация — элементы размещаются в памяти последовательно (для списков) или с хеш-кодами (для множеств и словарей)
- Создание ссылки — переменная получает адрес первого элемента
Основные типы коллекций в Kotlin:
| Тип | Описание | Особенности |
|---|---|---|
List |
Упорядоченный список (как очередь в магазине) | Есть индексы, можно повторять элементы |
Set |
Множество уникальных элементов (как набор марок) | Нет повторов, порядок не гарантирован |
Map |
Словарь "ключ-значение" (как телефонная книга) | Быстрый поиск по ключу |
Array |
Фиксированный массив (как поднос с ячейками) | Размер задаётся при создании |
Когда ты вызываешь метод коллекции, например .filter(), Kotlin внутри делает следующее:
- Создаёт новую пустую коллекцию
- Проходит по каждому элементу исходной коллекции
- Проверяет условие (предикат) для каждого элемента
- Если условие верно — добавляет элемент в новую коллекцию
- Возвращает новую коллекцию
⚡ Важно: Большинство методов коллекций в 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} |
Главные отличия:
- Типизация: Kotlin строго типизирован. Ты не сможешь положить строку в список чисел. Python — динамический, можно смешивать типы.
- Неизменяемость: В Kotlin коллекции по умолчанию неизменяемые. В Python списки всегда изменяемые.
- Синтаксис лямбд: В Kotlin лямбды пишутся в фигурных скобках
{ it -> ... }. В Python —lambda x: .... - Null-безопасность: В Kotlin методы могут возвращать
null(например,find), и компилятор заставит тебя это обработать. В Python — просто исключение. - Цепочки методов: В 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) {}
Вопросы для самопроверки
-
Вопрос: В чём разница между
listOfиmutableListOf?Ответ:
listOfсоздаёт неизменяемый список (нельзя добавлять/удалять элементы),mutableListOf— изменяемый. Неизменяемые коллекции безопаснее в многопоточном коде. -
Вопрос: Что вернёт
listOf(1, 2, 3).filter { it > 5 }?