Классы и коллекции
Зачем это нужно
В главах 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 Competitor(val name: String, var score: Int) - Метод внутри класса и значение по умолчанию
- data class: что генерирует ключевое слово
data - Списки:
ListиMutableList - Map: пары ключ — значение
- Set: только уникальные значения
- when: замена цепочке if / else if
- Типичные ошибки
- Символы главы
- Слова главы
- Потренируйся печатать
- Куда дальше: проверенные ресурсы
- Челлендж ⭐
- Что должен уметь
Класс и объект: чертёж и вещь по чертежу
Прежде чем писать код, зафиксируем два слова, которые легко перепутать.
- Класс (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.
Раз 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 («прибавь к текущему значению»), только слева от него теперь не голая переменная, а свойство объекта через точку. Работает это только потому, чтоscore—var: попробуй так же изменитьbogdan.name— получишь ошибку компиляции, разберём её в разделе «Типичные ошибки».
Заметь: сама переменная bogdan объявлена как val — и это не противоречие. val bogdan означает «ссылка на объект не поменяется» (мы не положим в bogdan другой объект), а не «объект нельзя менять изнутри». val фиксирует ссылку, а свойства внутри объекта живут своей жизнью, по своим правилам val/var.
Что выведет: 96 — потому что 91 + 5 = 96.
Метод внутри класса и значение по умолчанию
Класс может содержать не только свойства, но и функции — их называют методами (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 — иначе он не станет свойством и не попадёт ни в один из сгенерированных методов.
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().
- · Имя класса — с заглавной латинской буквы, например Competitor
- · Каждое поле названо с маленькой латинской буквы, например score
- · Имена полей не повторяются
data class ___(val ___: String, val ___: Int)Списки: List и MutableList
Теперь, когда есть тип для одного участника, соберём их в команду. List — упорядоченная коллекция элементов одного типа: у каждого элемента есть индекс (номер позиции), начиная с 0.
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 }filter { it.score >= 80 }map { it.name }Список из 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.
Что выведет: [Богдан, Соня] — «Соня» добавилась в конец, «Алиса» удалилась, порядок остальных элементов сохранился.
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 схлопнутся в один, как схлопнулись одинаковые числа выше. У обычного класса это не сработает: два объекта с одинаковыми полями будут считаться разными, потому что сравниваются по ссылке.
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останавливается на первой подошедшей ветке, как и версия без аргумента.
Что выведет: Призёр.
Типичные ошибки
Все сообщения ниже сняты вживую с той же песочницы 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-безопасность), если ты точно уверен, что ключ есть.
Символы главы
Шпаргалка по всем новым знакам и конструкциям этой главы.
| Символ / конструкция | Как называется | Что делает | Пример |
|---|---|---|---|
class | ключевое слово | объявляет класс — чертёж для создания объектов | class Competitor(...) |
val/var в скобках конструктора | свойство-параметр | параметр конструктора сразу становится свойством объекта, доступным через точку | class Competitor(val name: String) |
. после объекта | доступ к свойству/методу | читает свойство или вызывает метод конкретного объекта | bogdan.score |
data | ключевое слово | помечает класс данных: генерирует toString/equals/hashCode/copy/componentN | data 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 if | when (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, но с точным описанием, какие свойства учитываются, а какие — нет.
Слова главы
Потренируйся печатать
Сначала объявление класса с двумя свойствами в конструкторе:
Цель: скорость ≥ 110 зн/мин, точность ≥ 90%
class Competitor(val name: String, var score: Int)
Теперь карта с инфиксной функцией to:
Цель: скорость ≥ 120 зн/мин, точность ≥ 90%
val scores = mapOf("Алиса" to 82, "Богдан" to 91)
Куда дальше: проверенные ресурсы
Русские материалы с нуля
- kotlinlang.ru — Классы — официальная документация в переводе: конструкторы, свойства, наследование — читай раздел про основной конструктор и свойства.
- kotlinlang.ru — Классы данных — тот же перевод про data class: что именно генерируется и какие есть ограничения на параметры конструктора.
- kotlinlang.ru — Коллекции. Общий обзор — иерархия типов коллекций Kotlin: чем List/Set/Map отличаются от их изменяемых версий на уровне интерфейсов.
- kotlinlang.ru — Условия и циклы — в этой же главе официального перевода, ниже if, разобран when с примерами диапазонов и множественных веток.
- Урок 11: ООП. Классы и объекты в Kotlin — продолжение курса с уроков 1 и 10: классы объясняются через бытовые аналогии, с видео.
- Урок 9: Списки в Kotlin, функции для работы с коллекциями — тот же курс: List и операции над коллекциями отдельным уроком, с видео.
Официальный тур Kotlin (на английском)
- Kotlin tour: Classes — официальный тур: конструкторы, свойства и методы со встроенными упражнениями.
- Kotlin tour: Collections — официальный тур: List, Set, Map и их изменяемые версии на практических примерах.
Потренироваться
-
Kotlin Koans: Classes / Collections — в общем списке разделов Koans есть отдельные категории Classes и Collections — задачи с автопроверкой именно на темы этой главы.
-
Видео и материалы сообщества — ниже — ролики по теме и ссылки от студентов и преподавателей смотри в самом низу страницы.
Челлендж ⭐
Команда 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 — вход прямо в виджете выше.