Перейти к содержанию
Educora
Продвинутый24 мин7 / 14

Классы, data-классы, наследование и интерфейсы

Объединяй данные и поведение: классы, конструкторы, data-классы, `object` и объекты-компаньоны, наследование через `open` и интерфейсы.

Проверь себя
В этом уроке ты узнаешь
  • Писать класс с первичным конструктором, свойствами и методами
  • Пользоваться возможностями data-класса: toString, == и copy
  • Объяснять, зачем нужны object и объекты-компаньоны
  • Строить наследование через open/override и реализовывать интерфейс

Ты пишешь электронный журнал: у каждого ученика есть имя, класс и оценки, нужно добавлять новые оценки и считать средний балл. Для 30 учеников отдельные переменные быстро превращаются в путаницу. Объектно-ориентированное программирование предлагает другой путь: объединить данные и функции, работающие с ними, в одном месте — объекте. Kotlin делает это очень коротко.

Класс, конструктор и свойства

Класс — чертёж объектов, а объект — конкретный экземпляр, созданный по нему. В Kotlin первичный конструктор пишется в скобках сразу после имени класса: параметры, объявленные там через val или var, сразу становятся свойствами класса. Функции внутри класса называются методами. Объект создаётся без слова new, как вызов функции: Student("Aysel", 8).

Kotlin
class Student(val name: String, var grade: Int) {
    val scores = mutableListOf<Int>()

    fun addScore(score: Int) {
        scores.add(score)
    }

    fun average() = scores.average()

    fun introduce() = "I am $name, grade $grade"
}

fun main() {
    val aysel = Student("Aysel", 8)
    aysel.addScore(5)
    aysel.addScore(4)
    println(aysel.introduce())
    println(aysel.scores)
    println(aysel.average())
    aysel.grade = 9
    println(aysel.introduce())
}
Ожидаемый результат
I am Aysel, grade 8
[5, 4]
4.5
I am Aysel, grade 9
grade — это var, поэтому его можно менять, а name — val и не меняется.

Data-классы

Если вывести объект обычного класса, увидишь непонятный текст вроде Student@1b6d3586, а два объекта с одинаковыми данными не будут равны по ==. Очень часто класс нужен только для хранения данных — пользователь, товар, заказ. Тогда перед классом пишут слово data, и Kotlin автоматически создаёт читаемый toString, ==, сравнивающий содержимое, метод copy и разложение на компоненты.

Kotlin
data class User(val name: String, val age: Int)

fun main() {
    val u1 = User("Aysel", 15)
    val u2 = User("Aysel", 15)
    println(u1)
    println(u1 == u2)
    val older = u1.copy(age = 16)
    println(older)
    val (name, age) = older
    println("$name is $age")
}
Ожидаемый результат
User(name=Aysel, age=15)
true
User(name=Aysel, age=16)
Aysel is 16

object и объекты-компаньоны

Иногда в программе должен быть ровно один экземпляр чего-то: настройки приложения, подключение к базе данных. Для этого вместо class пишут object — так одновременно объявляется класс и создаётся его единственный объект. А companion object внутри класса хранит функции и константы, которые вызываются прямо по имени класса, без создания объекта, например «фабричные» функции, создающие объекты.

Kotlin
object Settings {
    var language = "az"
    fun describe() = "Language: $language"
}

class Temperature(val celsius: Double) {
    companion object {
        const val WATER_BOILS = 100.0
        fun fromFahrenheit(f: Double) = Temperature((f - 32) * 5 / 9)
    }
}

fun main() {
    println(Settings.describe())
    Settings.language = "en"
    println(Settings.describe())
    val t = Temperature.fromFahrenheit(212.0)
    println(t.celsius)
    println(t.celsius == Temperature.WATER_BOILS)
}
Ожидаемый результат
Language: az
Language: en
100.0
true

Наследование: open и override

Наследование позволяет создать новый класс на основе существующего: дочерний класс получает свойства и методы родителя. В Kotlin классы по умолчанию закрыты — чтобы от класса можно было наследоваться, его нужно пометить open. Метод, который дочерний класс может изменить, тоже должен быть open, а в дочернем классе его новая версия пишется с override. Родитель указывается после двоеточия: class Dog(name: String) : Animal(name).

Kotlin
open class Animal(val name: String) {
    open fun sound() = "..."
    fun introduce() = "$name says ${sound()}"
}

class Dog(name: String) : Animal(name) {
    override fun sound() = "Woof!"
}

class Cat(name: String) : Animal(name) {
    override fun sound() = "Meow!"
}

fun main() {
    val pets = listOf(Dog("Rex"), Cat("Mestan"), Animal("Nemo"))
    for (pet in pets) {
        println(pet.introduce())
    }
}
Ожидаемый результат
Rex says Woof!
Mestan says Meow!
Nemo says ...
Один и тот же introduce() вызывает у каждого объекта его собственную версию sound() — это называется полиморфизмом.

Интерфейсы

Интерфейс — это контракт, описывающий, что должны уметь классы: каждый класс, реализующий его свойства и методы, относится к типу этого интерфейса. Интерфейс может содержать и готовую реализацию метода. Класс может наследоваться только от одного класса, но реализовывать любое количество интерфейсов.

Kotlin
interface Shape {
    val name: String
    fun area(): Double
    fun describe() = "$name: area ${area()}"
}

class Rectangle(val width: Double, val height: Double) : Shape {
    override val name = "Rectangle"
    override fun area() = width * height
}

class Circle(val radius: Double) : Shape {
    override val name = "Circle"
    override fun area() = 3.14 * radius * radius
}

fun main() {
    val shapes: List<Shape> = listOf(Rectangle(3.0, 4.0), Circle(2.0))
    shapes.forEach { println(it.describe()) }
    println(shapes.maxBy { it.area() }.name)
}
Ожидаемый результат
Rectangle: area 12.0
Circle: area 12.56
Circle
Для простоты взято π ≈ 3,14; точное значение — kotlin.math.PI.
ОбъявлениеКогда использовать
classобычные объекты с данными и поведением
data classобъекты, которые в основном хранят данные
objectто, что существует в единственном экземпляре
open classбаза для дочерних классов
interfaceобщее умение нескольких классов (контракт)

Главное

  • class Student(val name: String) — первичный конструктор сразу объявляет свойство; объект создаётся без new.
  • data class автоматически получает toString, == по содержимому, copy и разложение на компоненты.
  • object создаёт единственный экземпляр, а companion object хранит члены, вызываемые по имени класса.
  • Классы и методы по умолчанию закрыты: для наследования нужен open, для переопределения — override.
  • Класс наследуется от одного родителя, но может реализовывать несколько интерфейсов.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
Что выведет println(Point(1, 2)) для data class Point(val x: Int, val y: Int)?