Перейти к основному содержимому
Мобилка05ГЛАВА 05Классы и коллекции

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

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

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

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

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

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

Хранить несколько объектов сразу — список участников, таблицу очков — мы научимся в следующей главе, про коллекции. А развёрнутое ООП с наследованием, интерфейсами и закрытыми иерархиями ждёт в главе «ООП на Kotlin».

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

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

  • Класс (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 («прибавь к текущему значению»), только слева от него теперь не голая переменная, а свойство объекта через точку. Работает это только потому, что score — var: попробуй так же изменить 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

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

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

Все сообщения ниже сняты вживую с той же песочницы 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.

Отвечено вопросов: 0 из 2
Компилятор сказал: No value passed for parameter 'score'. Что произошло?
Компилятор сказал: 'val' cannot be reassigned. Что делать?
Отвечено верно: 0 из 2

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

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

Символ / конструкцияКак называетсяЧто делаетПример
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
Углубиться

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

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

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

Слова главы​

class
1 / 8

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

Цель: скорость ≥ 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:

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

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

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

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

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

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

Сводная проверка по всей главе: 6 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.

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

  • Объявлять класс с основным конструктором и свойствами val/var, объясняя, что делает каждый символ, включая val/var внутри скобок.
  • Создавать объекты по классу и обращаться к их свойствам и методам через точку.
  • Добавлять классу метод и параметр со значением по умолчанию.
  • Объяснять, что генерирует ключевое слово data, и приводить разницу в выводе toString() и == между обычным классом и data class на конкретном примере.
  • Использовать .copy() для создания изменённой копии объекта, не трогая исходный.
  • Раскладывать объект по переменным через деструктуризацию и объяснять, откуда берётся эта возможность.
  • Узнавать по сообщению компилятора две типичные ошибки этой главы — непереданный аргумент конструктора и переприсваивание val-свойства — и чинить каждую.

Видео по теме

ВидеоKotlin: Урок 6. Работа с классами и их свойствамиDevcolibri
ВидеоУрок 11: ООП. Классы и свойства. Создание объектов (экземпляров класса)Kotlin & Android с нуля – Иван Ветров
ВидеоУрок 22: Data class (дата классы) copy, toString, equals, hashCodeKotlin & Android с нуля – Иван Ветров
ВидеоKOTLIN и ANDROID STUDIO для начинающих. Классы || Урок 22Neco Ru

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.