Перейти к основному содержимому
03МОБИЛКА · ГЛАВА 03Классы и коллекции

Классы и коллекции

Прочитано 0%Квизы 0/6Тренажёры 0/6Уровень 1 · Новичок

Зачем это нужно

В главах 1 и 2 мы хранили данные по одному: отдельная переменная name, отдельная переменная score. Пока участник один, это работает. Но чемпионат «Профессионалы» — это не один участник, а команда. Заводить name1, score1, name2, score2, name3, score3 и дальше вручную — плохая идея: код разрастается, а связь между именем и очками конкретного участника нигде не записана явно, её приходится держать в голове по номеру переменной.

Класс (class) решает эту задачу: это чертёж, который группирует связанные данные и поведение в один тип. Вместо name1 и score1 мы объявим один класс Competitor с полями «имя» и «очки», а дальше будем создавать сколько угодно объектов (object) — конкретных экземпляров (instance) — по этому чертежу. У каждого объекта свои значения полей, но структура одна и та же.

А чтобы работать сразу с несколькими объектами — командой из пяти, десяти, ста участников, — нужны коллекции (collections): List для упорядоченного списка, Map для пар «ключ — значение», Set для набора уникальных значений. И раз участников много, придётся на что-то реагировать по-разному в зависимости от результата — для этого в Kotlin есть when, более мощная замена цепочке if / else if.

В этой главе разберём: как объявить класс и создать объект, чем data class отличается от обычного класса и что именно генерирует слово data — увидим это не на словах, а в реальном выводе toString() и equals() — затем пройдём три основные коллекции и закончим when.

Класс и объект: чертёж и вещь по чертежу

Прежде чем писать код, зафиксируем два слова, которые легко перепутать.

  • Класс (class) — это чертёж, описание. Он не хранит конкретные данные, а описывает, какие данные и какое поведение будут у объектов этого типа. Так же как чертёж дома — не сам дом: по одному чертежу можно построить сколько угодно домов.
  • Объект (object), он же экземпляр (instance), — конкретная вещь, построенная по чертежу. У объекта уже есть настоящие значения: не «класс Competitor хранит имя», а «у этого конкретного объекта имя — „Алиса“».

Это то же самое различие, что между типом и значением из главы 1: String — тип, "Алиса" — значение этого типа. Класс — как бы «тип, который придумываешь сам», а объект — «значение этого типа».

  • Свойство (property) — данные внутри объекта, объявленные через val или var. У Competitor это будут name и score.
  • Метод (method) — функция, привязанная к классу. Мы уже вызывали чужие методы (.sum(), .map { } из глав 1–2); теперь научимся писать свои.

Первый класс: class Competitor(val name: String, var score: Int)

Вот минимальное объявление класса участника чемпионата. Разберём сигнатуру по символам.

Нажми на часть выражения

Что означает каждый символ

  • class — ключевое слово, как fun и val. Нельзя назвать так переменную или функцию.
  • Заглавная буква в имени класса — это соглашение (convention), не требование компилятора: код скомпилируется и с class competitor, но так в Kotlin не пишут. Competitor, String, Int — все типы начинаются с заглавной, а переменные и функции — с маленькой.
  • val/var внутри скобок конструктора — самое важное новое здесь. Без val/var то, что в скобках, было бы обычным параметром конструктора, доступным только внутри тела класса, но не снаружи через точку. С val/var это ещё и свойство (property): к нему можно обратиться у любого объекта как bogdan.name. Разница между val и var та же, что и у обычных переменных из главы 1: val-свойство нельзя переприсвоить после создания объекта, var-свойство — можно.
fun main() { val alice = Competitor("Алиса", 82) val bogdan = Competitor("Богдан", 91) println(alice.name) println(bogdan.score) } class Competitor(val name: String, var score: Int)

Разбор по строкам

  • Строка 2: val alice = Competitor("Алиса", 82) — создание объекта (instantiation): имя класса используется как вызов функции, аргументы в скобках попадают в свойства name и score по порядку, как обычные аргументы функции из главы 2. Слева — val alice: переменная, в которой лежит ссылка на новый объект. Заметь, что Competitor здесь пишется без new — в отличие от некоторых других языков (например, Java), в Kotlin создание объекта — это просто вызов имени класса.
  • Строка 3 — то же самое для bogdan, с другими значениями свойств.
  • Строка 4: println(alice.name) — точка после alice работает так же, как точка перед методом из главы 1 (listOf(1, 2).sum()), только здесь после неё не вызов метода, а обращение к свойству: «возьми свойство name у объекта alice».
  • Строка 5: println(bogdan.score) — то же самое для score объекта bogdan.
  • Строка 8: class Competitor(val name: String, var score: Int) — само объявление класса. Как и с функциями в главе 2, класс можно объявить после места, где он используется: компилятор сначала видит все объявления в файле, а потом проверяет тела.

Что выведет:

Алиса
91

Первая строка — свойство name объекта alice, вторая — свойство score объекта bogdan. Каждый объект хранит свою собственную копию свойств: значения alice никак не влияют на bogdan.

KOTLIN · КЛАСС — ЧЕРТЁЖ, ОБЪЕКТ — ВЕЩЬ ПО ЧЕРТЕЖУclass Competitor(name, score)alicename="Алиса", score=82bogdanname="Богдан", score=91один чертёж — сколько угодно объектов, у каждого свои значения полей
Один класс — чертёж; alice и bogdan — конкретные объекты по этому чертежу, каждый со своими значениями полей.

Раз score объявлен через var, его можно менять после создания объекта — снаружи, через точку, точно так же, как меняли обычную var-переменную в главе 1.

fun main() { val bogdan = Competitor("Богдан", 91) bogdan.score += 5 println(bogdan.score) } class Competitor(val name: String, var score: Int)

Разбор по строкам

  • Строка 3: bogdan.score += 5 — тот же оператор += из главы 1 («прибавь к текущему значению»), только слева от него теперь не голая переменная, а свойство объекта через точку. Работает это только потому, что scorevar: попробуй так же изменить bogdan.name — получишь ошибку компиляции, разберём её в разделе «Типичные ошибки».
Совет

Заметь: сама переменная bogdan объявлена как val — и это не противоречие. val bogdan означает «ссылка на объект не поменяется» (мы не положим в bogdan другой объект), а не «объект нельзя менять изнутри». val фиксирует ссылку, а свойства внутри объекта живут своей жизнью, по своим правилам val/var.

Что выведет: 96 — потому что 91 + 5 = 96.

Отвечено вопросов: 0 из 4
Что делает val перед параметром в class Competitor(val name: String, var score: Int)?
Почему можно написать val bogdan = Competitor(...), а потом bogdan.score += 5, если bogdan — val?
Что выведет println(alice.name), если alice = Competitor("Алиса", 82)?
Почему имя класса Competitor пишут с заглавной буквы, а имя переменной alice — с маленькой?
Отвечено верно: 0 из 4

Метод внутри класса и значение по умолчанию

Класс может содержать не только свойства, но и функции — их называют методами (methods), когда они принадлежат классу. Объявляются они так же, через fun, только внутри фигурных скобок тела класса — после списка свойств в скобках конструктора.

fun main() { val newcomer = Competitor("Соня") newcomer.addPoints(12) newcomer.addPoints(8) println("${newcomer.name}: ${newcomer.score}") } class Competitor(val name: String, var score: Int = 0) { fun addPoints(points: Int) { score += points } }

Разбор по строкам

  • Строка 8: class Competitor(val name: String, var score: Int = 0) { — у score появилось значение по умолчанию (default argument) — то же самое = 0, что мы разбирали для параметров функций в главе 2. Раз оно есть, score можно не передавать при создании объекта. После скобок конструктора появилась { — тело класса: место для методов и дополнительной логики.
  • Строка 9: fun addPoints(points: Int) { — метод внутри тела класса. Выглядит как обычная функция из главы 2, только теперь у неё есть доступ к свойствам того объекта, у которого её вызвали.
  • Строка 10: score += points — внутри метода можно обращаться к score без this. и точки: Kotlin понимает, что раз мы внутри класса Competitor, score — это свойство текущего объекта. this (ключевое слово «этот объект») можно писать явно — this.score — но когда путаницы с одноимённым параметром нет, это не обязательно.
  • Строка 2: val newcomer = Competitor("Соня") — создаём объект, передав только name. score не указан — берётся значение по умолчанию 0, точно как с параметрами функций.
  • Строки 3–4: newcomer.addPoints(12) / newcomer.addPoints(8) — вызов метода: объект.метод(аргументы), тот же синтаксис, что и у чужих методов вроде .sum().

Что выведет: Соня: 20 — начали с score = 0 (по умолчанию), прибавили 12, потом 8: 0 + 12 + 8 = 20.

data class: что генерирует ключевое слово data

У обычного класса Competitor есть неудобство: если попробовать напечатать объект напрямую или сравнить два объекта с одинаковыми полями, результат окажется не таким, как ожидаешь. Слово data перед class чинит это — компилятор автоматически дописывает за нас несколько методов. Разберём каждый по отдельности, с реальным выводом.

Нажми на часть выражения
Важно

Правило, о котором легко забыть: у data class в основном конструкторе должен быть хотя бы один параметр, и каждый параметр обязан быть val или var — иначе он не станет свойством и не попадёт ни в один из сгенерированных методов.

KOTLIN · data class — ЧТО ДОПИСЫВАЕТ КОМПИЛЯТОРdata class Competitor(name, score)toString()equals()hashCode()copy()componentN()одно слово data — пять сгенерированных методов, без единой строчки от тебя
Одно слово data перед class — и компилятор сам дописывает toString, equals, hashCode, copy и componentN.

toString(): как объект печатает себя

Сначала — что выводит обычный класс, если передать объект прямо в println, не залезая в свойства по отдельности.

fun main() { val bogdan = Competitor("Богдан", 91) println(bogdan) } class Competitor(val name: String, var score: Int)

Что выведет: что-то вроде Competitor@2c7b84de — имя класса, знак @ и шестнадцатеричный адрес объекта в памяти. У тебя это будет другое число: адрес каждый раз новый, он ничего не говорит о содержимом объекта. Это стандартный toString(), унаследованный от Any (базового типа всех классов в Kotlin) — он не знает, что внутри объекта, и просто печатает служебную метку.

Теперь та же программа, только с data перед class:

fun main() { val bogdan = Competitor("Богдан", 91) println(bogdan) } data class Competitor(val name: String, var score: Int)

Что выведет: Competitor(name=Богдан, score=91) — читаемое представление: имя класса и все свойства с их значениями. Это и есть сгенерированный toString() — он написан за нас, потому что мы пометили класс словом data.

equals(): по ссылке или по содержимому

Ещё заметнее разница проявляется при сравнении ==. Два объекта обычного класса с одинаковыми полями:

fun main() { val a = Competitor("Богдан", 91) val b = Competitor("Богдан", 91) println(a == b) } class Competitor(val name: String, var score: Int)

Что выведет: false. У обычного класса == по умолчанию сравнивает ссылки (referential equality): «это буквально один и тот же объект в памяти?» a и b — два разных объекта, просто с одинаковым содержимым полей, поэтому сравнение — false, даже несмотря на одинаковые name и score.

Тот же код, но класс — data class:

fun main() { val a = Competitor("Богдан", 91) val b = Competitor("Богдан", 91) println(a == b) } data class Competitor(val name: String, var score: Int)

Что выведет: true. Сгенерированный equals() сравнивает не ссылки, а содержимое (structural equality): «одинаковы ли значения всех свойств?» У a и b они совпадают — значит, объекты равны, даже если это два разных объекта в памяти. Вместе с equals() компилятор всегда генерирует и согласованный hashCode() — это важно, если объекты потом кладут в Set или используют как ключи Map (увидим дальше в главе): без согласованных equals()/hashCode() такие коллекции будут вести себя непредсказуемо.

copy(): новый объект с изменённым полем

У data class появляется ещё один сгенерированный метод — copy(): создаёт новый объект на основе старого, позволяя заменить только нужные свойства.

fun main() { val bogdan = Competitor("Богдан", 91) val bogdanNextRound = bogdan.copy(score = 96) println(bogdan) println(bogdanNextRound) } data class Competitor(val name: String, var score: Int)

Разбор по строкам

  • Строка 3: bogdan.copy(score = 96)copy() принимает именованные аргументы (named arguments, глава 2) для тех свойств, которые нужно поменять; остальные копируются как есть. Здесь передан только score — значит, name в новом объекте останется тем же, "Богдан".
  • Важно: copy() создаёт новый объект, bogdan не меняется. Это видно по выводу: bogdan остался со score = 91, а bogdanNextRound — отдельный объект со score = 96.

Что выведет:

Competitor(name=Богдан, score=91)
Competitor(name=Богдан, score=96)

Проверь себя на связке toString() + copy() — сначала предскажи обе строки вывода, потом запускай:

fun main() {
  val bogdan = Competitor("Богдан", 91)
  val next = bogdan.copy(score = 96)
  println(next)
  println(bogdan.score)
}

data class Competitor(val name: String, var score: Int)

Что напечатает этот код? Сначала ответь, потом проверяй.

Бонус: деструктуризация через componentN()

Последний сгенерированный кусок — функции component1(), component2() и так далее, по одной на каждое свойство основного конструктора, в порядке объявления. Их редко вызывают напрямую — вместо этого есть удобный синтаксис: разложить объект на отдельные переменные одной строкой.

fun main() { val bogdan = Competitor("Богдан", 91) val (name, score) = bogdan println("$name -> $score") } data class Competitor(val name: String, var score: Int)

Что выведет: Богдан -> 91. val (name, score) = bogdan — деструктуризация (destructuring declaration): name берётся из component1() (первое свойство, name), score — из component2() (второе свойство, score). Работает это только благодаря data: у обычного класса component1()/component2() не сгенерированы, и такая запись не скомпилируется.

Теперь собери свой data class сам: задай имя и поля — и посмотри живьём, что дадут сгенерированные toString(), equals() и copy().

Имя класса
Поле 1
Поле 2
  • · Имя класса — с заглавной латинской буквы, например Competitor
  • · Каждое поле названо с маленькой латинской буквы, например score
  • · Имена полей не повторяются
data class ___(val ___: String, val ___: Int)
Заполни имя класса и поля по правилам выше — примеры появятся здесь.
Отвечено вопросов: 0 из 4
Что печатает println(bogdan) для обычного (не data) класса?
Почему a == b выводит false для обычного класса, даже если у a и b одинаковые name и score?
Что делает bogdan.copy(score = 96)?
Что обязательно должно быть верно для параметров основного конструктора data class?
Отвечено верно: 0 из 4

Списки: List и MutableList

Теперь, когда есть тип для одного участника, соберём их в команду. List — упорядоченная коллекция элементов одного типа: у каждого элемента есть индекс (номер позиции), начиная с 0.

KOTLIN · КОЛЛЕКЦИИ — ТРИ ПОЛКИList[0]Алиса[1]Богда[2]Соняпорядок и индекс важныMapАлиса82Богдан91Соня77доступ по ключу, не по номеруSet82917782повтор 82 внутрь не попал — набор без дублей
Три полки для данных: List хранит по порядку и номеру, Map — по ключу, Set — только уникальные значения без порядка.
Нажми на часть выражения
fun main() { val names = listOf("Алиса", "Богдан", "Соня") for (name in names) { println(name) } println(names[1]) println(names.size) }

Разбор по строкам

  • Строка 2: listOf("Алиса", "Богдан", "Соня") — функция-фабрика (как mapOf/setOf дальше в главе): создаёт List<String> — список строк. Список, созданный через listOf, — read-only: в него нельзя ни добавить, ни убрать элемент (попробуем и увидим ошибку компиляции в разделе «Типичные ошибки»).
  • Строки 3–5: for (name in names) { println(name) } — цикл проходит по всем трём элементам по очереди: сначала name = "Алиса", затем name = "Богдан", затем name = "Соня", печатая каждый.
  • Строка 6: names[1] — новый символ, квадратные скобки [ ] — оператор индексации (index operator): достаёт элемент списка по номеру позиции. Отсчёт всегда идёт с нуля: names[0] — «Алиса», names[1] — «Богдан», names[2] — «Соня». names[1] — это не то же самое, что «второй по счёту» в обычной речи, а «элемент под индексом 1».
  • Строка 7: names.size — свойство size (без скобок — это не метод, а свойство, готовое значение) хранит количество элементов.

Что выведет:

Алиса
Богдан
Соня
Богдан
3

Первые три строки — цикл, четвёртая — names[1] («Богдан» под индексом 1), пятая — names.size (в списке три элемента).

Списки можно строить и из своих классов — например, из Competitor, а не только из строк и чисел. Тогда к каждому элементу применимы уже знакомые из главы 1 map/filter/sum.

fun main() { val team = listOf( Competitor("Алиса", 82), Competitor("Богдан", 91), Competitor("Соня", 77) ) val totalScore = team.map { it.score }.sum() println(totalScore) } data class Competitor(val name: String, var score: Int)

Разбор по строкам

  • Строки 2–6: val team = listOf(...) — список из трёх объектов Competitor, каждый со своим именем и очками. Список можно писать в несколько строк — запятая после каждого элемента, кроме последнего, работает так же, как и в одну строку.
  • Строка 7: team.map { it.score }.sum() — та же цепочка map/sum из главы 1, только теперь it — не число, а целый объект Competitor. it.score достаёт из каждого объекта одно свойство, map собирает из этих свойств новый список чисел, sum() складывает их.

Что выведет: 250 — потому что 82 + 91 + 77 = 250.

Та же команда, но теперь цепочка filter/map из главы 1 — предскажи, какой список напечатается:

fun main() {
  val team = listOf(
      Competitor("Алиса", 82),
      Competitor("Богдан", 91),
      Competitor("Соня", 77)
  )
  val qualified = team.filter { it.score >= 80 }.map { it.name }
  println(qualified)
}

data class Competitor(val name: String, var score: Int)

Что напечатает этот код? Сначала ответь, потом проверяй.

Та же цепочка — теперь в замедленной съёмке: прогони каждый элемент через конвейер filter/map по шагам и посмотри, где элемент отбрасывается, а где превращается.

team.filter { it.score >= 80 }.map { it.name }
team
Алиса · 82Богдан · 91Соня · 77
filter { it.score >= 80 }
после filter
пока пусто
map { it.name }
qualified
пока пусто
Та же цепочка, что в примере выше: filter отбирает участников с score >= 80, map оставляет только имена. Жми «Шаг» — элементы пойдут по конвейеру по одному.
Шаг 0 из 5

Список из listOf — неизменяемый (в том смысле, что через сам этот List нельзя ни добавить, ни удалить элемент). Когда список должен расти и меняться, используют mutableListOf — тот же список, но с методами add/remove.

fun main() { val roster = mutableListOf("Алиса", "Богдан") roster.add("Соня") roster.remove("Алиса") println(roster) }

Разбор по строкам

  • Строка 2: mutableListOf("Алиса", "Богдан") — создаёт MutableList<String>: список с теми же методами, что у обычного List (size, [индекс], map, filter...), плюс методы изменения.
  • Строка 3: roster.add("Соня") — добавляет элемент в конец списка.
  • Строка 4: roster.remove("Алиса") — убирает из списка первое вхождение значения "Алиса" (не по индексу, а по значению).
  • Строка 5: println(roster) — списки печатаются в квадратных скобках через запятую — то же текстовое представление, что мы уже видели в главе 1 у результата map.

Что выведет: [Богдан, Соня] — «Соня» добавилась в конец, «Алиса» удалилась, порядок остальных элементов сохранился.

Отвечено вопросов: 0 из 4
Что означает for (name in names)?
Чем names[1] отличается от «второго элемента» в обычной речи?
Чем listOf(...) отличается от mutableListOf(...)?
Что выведет team.map { it.score }.sum(), если team — список из трёх объектов Competitor?
Отвечено верно: 0 из 4

Map: пары ключ — значение

Map хранит не просто значения, а пары «ключ → значение» (key-value pair): вместо доступа по номеру позиции, как у List, доступ идёт по ключу — например, по имени участника.

Нажми на часть выражения
fun main() { val scores = mapOf("Алиса" to 82, "Богдан" to 91, "Соня" to 77) println(scores["Богдан"]) for ((name, score) in scores) { println("$name: $score") } }

Разбор по строкам

  • Строка 2 — создание карты: три пары «имя → очки».
  • Строка 3: scores["Богдан"] — тот же оператор индексации [ ], что и у списков, только теперь в скобках не номер позиции, а ключ. Возвращает значение, связанное с этим ключом.
  • Строка 4: for ((name, score) in scores) — перебор карты: на каждой итерации for даёт не один элемент, а пару, которую сразу деструктурируют (та же деструктуризация, что мы видели у data class) на name (ключ) и score (значение).
  • Строка 5: println("$name: $score") — строковый шаблон из главы 1, теперь с двумя подстановками подряд.

Что выведет:

91
Алиса: 82
Богдан: 91
Соня: 77

Первая строка — прямой доступ по ключу "Богдан". Дальше — цикл: mapOf сохраняет порядок, в котором пары были перечислены при создании, поэтому вывод идёт именно в порядке Алиса → Богдан → Соня, а не в каком-то случайном.

Как и у List, есть изменяемая версия — MutableMap, с методами добавления и обновления записей.

fun main() { val scores = mutableMapOf("Алиса" to 82, "Богдан" to 91) scores["Соня"] = 77 scores["Алиса"] = 85 println(scores) println(scores.getOrDefault("Дима", 0)) }

Разбор по строкам

  • Строка 3: scores["Соня"] = 77 — новая роль тех же квадратных скобок: индексация слева от = не читает значение, а кладёт его в карту. Если ключа "Соня" ещё не было — добавляет новую пару; если был бы — заменил бы значение.
  • Строка 4: scores["Алиса"] = 85 — ключ "Алиса" уже существовал (со значением 82), поэтому это не добавление, а замена: новое значение 85 затирает старое 82.
  • Строка 6: scores.getOrDefault("Дима", 0) — безопасный способ спросить значение по ключу, которого может не быть. Если бы мы написали просто scores["Дима"], то получили бы null («ничего») — про null в этом курсе будет отдельная глава, а пока getOrDefault обходит эту проблему: если ключа нет, возвращает второй аргумент как запасное значение.

Что выведет:

{Алиса=85, Богдан=91, Соня=77}
0

Карта печатается в фигурных скобках, пары через запятую в формате ключ=значение. "Алиса" теперь 85, а не 82 — значение заменилось. Для несуществующего ключа "Дима" вернулся запасной 0.

Set: только уникальные значения

Set — коллекция без повторов: если добавить значение, которое уже есть, ничего не произойдёт — набор просто не станет длиннее.

fun main() { val uniqueScores = setOf(82, 91, 82, 77, 91) println(uniqueScores) println(uniqueScores.size) }

Разбор по строкам

  • Строка 2: setOf(82, 91, 82, 77, 91) — передали пять чисел, но 82 и 91 встречаются дважды. Set хранит только уникальные значения, поэтому повторы схлопываются в один экземпляр каждое.
  • Строка 3–4 — печать: Set, как и List, выводится в квадратных скобках, порядок — тот, в котором значения встретились впервые.

Что выведет:

[82, 91, 77]
3

Из пяти переданных чисел осталось три уникальных: 82 и 91 каждое учтено один раз, несмотря на то что были указаны дважды. Как и у списков и карт, есть mutableSetOf с методами add/remove — работает по тем же правилам, что и mutableListOf.

Важно

Именно здесь особенно важна связка equals()/hashCode() из раздела про data class: Set и ключи Map определяют уникальность через них. У data class она согласована автоматически — два структурно одинаковых объекта Competitor в Set схлопнутся в один, как схлопнулись одинаковые числа выше. У обычного класса это не сработает: два объекта с одинаковыми полями будут считаться разными, потому что сравниваются по ссылке.

Отвечено вопросов: 0 из 4
Что делает "Алиса" to 82 в mapOf("Алиса" to 82)?
Что делает scores["Соня"] = 77 для MutableMap?
Что выведет setOf(82, 91, 82, 77, 91)?
Почему getOrDefault("Дима", 0) безопаснее, чем scores["Дима"], если ключа "Дима" нет?
Отвечено верно: 0 из 4

when: замена цепочке if / else if

Когда вариантов больше двух, цепочка if / else if / else быстро становится длинной и трудночитаемой. when — конструкция Kotlin для точно такой задачи: выбрать один вариант из нескольких.

Нажми на часть выражения
fun main() { val score = 91 val level = when { score >= 90 -> "Золото" score >= 75 -> "Серебро" score >= 50 -> "Бронза" else -> "Участие" } println(level) }

Разбор по строкам

  • Строка 3: val level = when { ... }when используется как выражение (expression): результат кладётся прямо в переменную — так же, как в главе 2 в переменную попадал результат функции с телом-выражением.
  • Строки 4–6 — ветки проверяются по порядку сверху вниз. when без аргумента в скобках — это по сути список независимых условий: как только одно оказывается истинным, выполняется его ветка, а остальные — даже не проверяются.
  • Для score = 91: первое условие score >= 90 уже истинно, поэтому результат — "Золото", а до score >= 75 дело даже не доходит.

Что выведет: Золото.

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

Нажми на часть выражения
fun main() { val place = 2 val medal = when (place) { 1 -> "Золото" 2, 3 -> "Призёр" in 4..10 -> "Финалист" else -> "Участник" } println(medal) }

Разбор по строкам

  • Строка 2: val place = 2 — участник занял второе место.
  • Строка 4: 2, 3 -> "Призёр"place сравнивается с каждым значением через запятую; place == 2 истинно, значит эта ветка и сработает.
  • Строки 1 -> ... выше и in 4..10 -> ... ниже даже не проверяются дальше — when останавливается на первой подошедшей ветке, как и версия без аргумента.

Что выведет: Призёр.

Отвечено вопросов: 0 из 4
Чем when { score >= 90 -> ... } (без скобок после when) отличается от when (place) { 1 -> ... }?
Что делает ветка 2, 3 -> "Призёр" в when (place) { ... }?
Зачем нужна ветка else, если when используется как выражение (val x = when { ... })?
Что проверяет in 4..10 в ветке when?
Отвечено верно: 0 из 4

Типичные ошибки

Все сообщения ниже сняты вживую с той же песочницы Kotlin Playground, что работает в кнопках Run этой главы — тексты приведены ровно такими, какими их показывает компилятор.

1. Не передан аргумент конструктора

fun main() { val newcomer = Competitor("Дима") println(newcomer.score) } class Competitor(val name: String, var score: Int)
No value passed for parameter 'score'.

Перевод: «Не передано значение для параметра score». У score в этом классе нет значения по умолчанию — он обязателен, а конструктор вызван только с одним аргументом.

Как починить: передать оба аргумента — Competitor("Дима", 0), — либо дать score значение по умолчанию в объявлении класса: var score: Int = 0.

2. Переприсваивание val-свойства

fun main() { val bogdan = Competitor("Богдан", 91) bogdan.name = "Николай" } class Competitor(val name: String, var score: Int)
'val' cannot be reassigned.

Перевод: «val нельзя переприсвоить». Та же ошибка, что мы видели в главе 1 для обычной переменной — только теперь name не отдельная переменная, а val-свойство объекта. Правило то же: val фиксируется один раз и навсегда, у него просто нет сеттера (setter) — функции, которая могла бы записать новое значение.

Как починить: если имя участника действительно должно меняться — объявить свойство как var name: String. Если нет — не переприсваивать, а создать новый объект, например через .copy(name = "Николай"), если класс — data class.

3. add() у List, а не у MutableList

fun main() { val names: List<String> = listOf("Алиса", "Богдан") names.add("Соня") }
Unresolved reference 'add'.

Перевод: «Неразрешённая ссылка add». List — интерфейс только для чтения: у него нет метода add, он просто не существует у этого типа, поэтому компилятор его не находит — так же, как не нашёл бы опечатку в имени переменной.

Как починить: объявить список как изменяемый — val names: MutableList<String> = mutableListOf("Алиса", "Богдан"), — и тогда add появится.

4. when-выражение без else

fun main() { val place = 2 val medal = when (place) { 1 -> "Золото" 2 -> "Серебро" } println(medal) }
'when' expression must be exhaustive. Add an 'else' branch.

Перевод: «when-выражение должно быть исчерпывающим (exhaustive). Добавьте ветку else». place — обычный Int, у него бесконечно много возможных значений, а перечислены только 1 и 2. Раз результат when кладётся в val medal, компилятору нужна гарантия, что значение появится при любом place, — без else такой гарантии нет.

Как починить: добавить else -> "Участник" последней веткой.

5. Map возвращает значение, которого может не быть

fun main() { val scores = mapOf("Алиса" to 82, "Богдан" to 91) val aliceScore: Int = scores["Алиса"] println(aliceScore) }
Initializer type mismatch: expected 'Int', actual 'Int?'.

Перевод: «Несовпадение типа начального значения: ожидался Int, а по факту Int?». Обращение к карте по ключу через [ ] всегда возвращает «может быть Int, а может быть ничего» (Int? — nullable-тип, потому что ключа могло и не быть) — даже если ключ в этот раз точно есть. Компилятор судит по типу, а не по конкретным данным, а тип scores["Алиса"] — всегда Int?, не Int.

Как починить: либо изменить тип переменной на Int?val aliceScore: Int? = scores["Алиса"], — либо использовать getOrDefault("Алиса", 0) или оператор !! (разберём в будущей главе про null-безопасность), если ты точно уверен, что ключ есть.

KOTLIN · Map[ключ] ВОЗВРАЩАЕТ Int?, НЕ IntInt82значение есть всегдаInt?82 или nullключа могло не быть — тип честно предупреждаеткомпилятор не пустит Int? туда, где обещан гарантированный Int
Обращение к Map по ключу возвращает Int?, а не Int: значение может быть, а может — не быть, и тип честно об этом предупреждает.
Отвечено вопросов: 0 из 4
Почему Competitor("Дима") выдаёт "No value passed for parameter 'score'."?
Почему bogdan.name = "Николай" не компилируется для class Competitor(val name: String, ...)?
Почему names.add("Соня") не работает, если names: List<String> = listOf(...)?
Почему scores["Алиса"] нельзя напрямую положить в val x: Int?
Отвечено верно: 0 из 4

Символы главы

Шпаргалка по всем новым знакам и конструкциям этой главы.

Символ / конструкцияКак называетсяЧто делаетПример
classключевое словообъявляет класс — чертёж для создания объектовclass Competitor(...)
val/var в скобках конструкторасвойство-параметрпараметр конструктора сразу становится свойством объекта, доступным через точкуclass Competitor(val name: String)
. после объектадоступ к свойству/методучитает свойство или вызывает метод конкретного объектаbogdan.score
dataключевое словопомечает класс данных: генерирует toString/equals/hashCode/copy/componentNdata class Competitor(...)
==оператор равенствау data class — по содержимому полей, у обычного класса — по ссылкеa == b
.copy(поле = значение)метод data classсоздаёт новый объект на основе старого с изменёнными полямиbogdan.copy(score = 96)
val (a, b) = objдеструктуризацияраскладывает свойства объекта по переменным через componentN()val (name, score) = bogdan
for (x in coll) { }цикл по коллекцииприсваивает x каждый элемент коллекции по очередиfor (name in names)
[индекс] / [ключ]оператор индексациидостаёт элемент списка по позиции или значение карты по ключуnames[1], scores["Алиса"]
toинфиксная функциясоздаёт пару ключ-значение (Pair) для mapOf"Алиса" to 82
[ключ] = значениеиндексация со присваиваниемдобавляет или заменяет значение в MutableMap по ключуscores["Соня"] = 77
whenключевое слововыбирает одну ветку из нескольких — замена цепочке if/else ifwhen (place) { ... }
, в ветке whenгруппировка значенийодна ветка срабатывает для нескольких перечисленных значений2, 3 -> "Призёр"
in a..b в ветке whenпроверка диапазонаветка срабатывает, если значение входит в диапазонin 4..10 -> "Финалист"
Углубиться

Почему equals() и hashCode() всегда генерируются парой. Контракт (contract) между этими двумя методами простой: если a == b, то у них обязан совпадать и hashCode(). Set и Map под капотом используют hashCode(), чтобы быстро понять, в какую «корзину» положить объект, и только потом проверяют equals() внутри неё. Если бы equals() сравнивал по содержимому, а hashCode() — по ссылке (как это можно сделать вручную, переопределив только один из двух), одинаковые по значению объекты могли бы попасть в разные «корзины» и Set считал бы их разными, несмотря на ==, вернувший true. Именно поэтому data class генерирует оба метода одновременно и никогда — только один из них.

List — это интерфейс, а не гарантия неизменности. listOf(...) возвращает объект, у типа List которого просто нет методов изменения — но это ограничение на уровне доступа через этот тип, а не свойство самих данных в памяти. Если тот же набор элементов виден откуда-то ещё и через MutableList (например, mutableListOf присвоили переменной типа List), изменить его всё ещё можно — просто не через ссылку List. Для настоящей неизменности данных в памяти в Kotlin есть отдельные immutable-структуры за пределами базового курса.

Дальше по теме:

  • Equality — официальная документация: структурное и ссылочное равенство, контракт equals()/hashCode() подробно.
  • Destructuring declarations — официальная документация про componentN() и деструктуризацию не только объектов, но и Map, и Pair.
  • Data classes — официальная документация на английском: те же toString/equals/copy, но с точным описанием, какие свойства учитываются, а какие — нет.

Слова главы

class
1 / 10

Потренируйся печатать

Сначала объявление класса с двумя свойствами в конструкторе:

Цель: скорость ≥ 110 зн/мин, точность ≥ 90%

class Competitor(val name: String, var score: Int)
клавиша ``клавиша 11клавиша 22клавиша 33клавиша 44клавиша 55клавиша 66клавиша 77клавиша 88клавиша 99клавиша 00клавиша --клавиша ==клавиша ⌫клавиша TabTabклавиша QQклавиша WWклавиша EEклавиша RRклавиша TTклавиша YYклавиша UUклавиша IIклавиша OOклавиша PPклавиша [[клавиша ]]клавиша \\клавиша Caps LockCaps Lockклавиша AAклавиша SSклавиша DDклавиша FFклавиша GGклавиша HHклавиша JJклавиша KKклавиша LLклавиша ;;клавиша ''клавиша EnterEnterклавиша ShiftShiftклавиша ZZклавиша XXклавиша CCклавиша VVклавиша BBклавиша NNклавиша MMклавиша ,,клавиша ..клавиша //клавиша ShiftShiftклавиша CtrlCtrlклавиша WinWinклавиша AltAltклавиша ПробелПробелклавиша AltAltклавиша WinWinклавиша MenuMenuклавиша CtrlCtrl
следующая: C — средний левой руки

Теперь карта с инфиксной функцией to:

Цель: скорость ≥ 120 зн/мин, точность ≥ 90%

val scores = mapOf("Алиса" to 82, "Богдан" to 91)

Куда дальше: проверенные ресурсы

Русские материалы с нуля

Официальный тур Kotlin (на английском)

  • Kotlin tour: Classes — официальный тур: конструкторы, свойства и методы со встроенными упражнениями.
  • Kotlin tour: Collections — официальный тур: List, Set, Map и их изменяемые версии на практических примерах.

Потренироваться

play.kotlinlang.org
Та же песочница, что работает в кнопках Run этой главы, но во весь экран.

Челлендж ⭐

Команда Competitor уже знакома: Алиса — 82 очка, Богдан — 91, Соня — 66.

Ступень 1. Организаторам нужен список имён тех, кто прошёл отбор — набрал score >= 75. Напиши одно выражение на цепочке .filter { }.map { } (по образцу из этой главы и главы 1), которое из списка team отбирает нужных участников и оставляет только их имена.

Ступень 2 ⭐. Напиши функцию medalFor(score: Int): String, использующую when с диапазонами (>= 90"Золото", >= 75"Серебро", >= 50"Бронза", иначе — "Участие"), и выведи для каждого участника команды строку вида Богдан: Золото.

fun main() { val team = listOf( Competitor("Алиса", 82), Competitor("Богдан", 91), Competitor("Соня", 66) ) // ступень 1: замени emptyList() на список имён с score >= 75 val qualified = emptyList<String>() println("Прошли отбор: $qualified") // ступень 2 (со звёздочкой): допиши medalFor и выведи медаль каждого участника } data class Competitor(val name: String, var score: Int) // ступень 2 (со звёздочкой): допиши функцию здесь // fun medalFor(score: Int): String = when { ... }
Решение (сначала попробуй сам)

Ступень 1:

val qualified = team.filter { it.score >= 75 }.map { it.name }

Ступень 2 ⭐:

fun medalFor(score: Int): String = when {
score >= 90 -> "Золото"
score >= 75 -> "Серебро"
score >= 50 -> "Бронза"
else -> "Участие"
}

fun main() {
val team = listOf(
Competitor("Алиса", 82),
Competitor("Богдан", 91),
Competitor("Соня", 66)
)
val qualified = team.filter { it.score >= 75 }.map { it.name }
println("Прошли отбор: $qualified")
for (competitor in team) {
println("\${competitor.name}: \${medalFor(competitor.score)}")
}
}

data class Competitor(val name: String, var score: Int)

Полный вывод:

Прошли отбор: [Алиса, Богдан]
Алиса: Серебро
Богдан: Золото
Соня: Бронза

Алиса (82) и Богдан (91) прошли отбор — оба набрали 75 и больше; Соня с 66 — нет, поэтому в qualified её не будет, но when всё равно посчитает ей медаль «Бронза» (66 ≥ 50) при выводе полного списка.

Что должен уметь

  • Объявлять класс с основным конструктором и свойствами val/var, объясняя, что делает каждый символ, включая val/var внутри скобок.
  • Создавать объекты по классу и обращаться к их свойствам и методам через точку.
  • Объяснять, что генерирует ключевое слово data, и приводить разницу в выводе toString() и == между обычным классом и data class на конкретном примере.
  • Использовать .copy() для создания изменённой копии объекта, не трогая исходный.
  • Отличать List от MutableList и Map от MutableMap по набору доступных методов, объясняя, почему .add() не работает на List.
  • Создавать и обходить List, Map и Set, используя [индекс]/[ключ], for (x in coll) и деструктуризацию for ((k, v) in map).
  • Строить пары ключ-значение через инфиксную функцию to и безопасно читать значение из карты через getOrDefault.
  • Писать when и как список независимых условий, и как сравнение одного значения с ветками через запятую и диапазоны in a..b.
  • Объяснять, почему when-выражение требует ветку else для исчерпывающести (exhaustiveness), когда его результат кладётся в переменную.
  • Узнавать по сообщению компилятора пять типичных ошибок этой главы — непереданный аргумент конструктора, переприсваивание val-свойства, вызов .add() у List, неисчерпывающий when, обращение к Map без учёта null — и чинить каждую.

Комментарии

Комментарии появятся после настройки. Нужен аккаунт GitHub — вход прямо в виджете выше.