Базовый синтаксис классов #

Васильев Андрей Михайлович

Версии презентации

Классы #

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

Классы в Kotlin объявляются с помощью использования ключевого слова class.

class Person { /*...*/ }

Объявление класса состоит из

  • имени класса
  • заголовка (указания типов его параметров, основного конструктора и т.п)
  • и тела класса, заключённого в фигурные скобки

И заголовок, и тело класса являются необязательными составляющими. Если у класса нет тела, фигурные скобки могут быть опущены

class Empty

Конструкторы #

Класс в Kotlin может иметь основной конструктор (primary constructor) и один или более дополнительных конструкторов (secondary constructors)

Основной конструктор является частью заголовка класса, его объявление идёт сразу после имени класса (и необязательных параметров типа).

class Person constructor(firstName: String) { /*...*/ }

Если у основного конструктора нет аннотаций и модификаторов видимости, ключевое слово constructor может быть опущено.

class Person(firstName: String) { /*...*/ }

Ключевое слово constructor обязательно, если у основного конструктора есть аннотация или модификатор видимости:

class Person private constructor(val firstName: String) { /*...*/ }

Дополнительные конструкторы объявляются в теле класса и обязаны делегировать работу основному конструктору через this:

class Person(val firstName: String) {
    constructor(firstName: String, lastName: String) : this(firstName) {
        println("Дополнительный конструктор: $lastName")
    }
}

Инициализация #

Основной конструктор не может содержать в себе исполняемого кода. Инициализирующий код может быть помещён в соответствующие блоки (initializers blocks), которые помечаются словом init.

При создании экземпляра класса блоки инициализации выполняются в том порядке, в котором они идут в теле класса, чередуясь с инициализацией свойств.

class InitOrderDemo(name: String) {
    val firstProperty = "Первое свойство: $name"
    
    init {
        println("Первый блок инициализации: ${name}")
    }
    
    val secondProperty = "Второе свойство: ${name.length}"
    
    init {
        println("Второй блок инициализации: ${name.length}")
    }
}

fun main() {
    InitOrderDemo("hello")
}

Вывод приложения:

Первый блок инициализации: hello
Второй блок инициализации: 5

Инициализация свойств #

Обратите внимание, что параметры основного конструктора могут быть использованы в инициализирующем блоке. Они также могут быть использованы при инициализации свойств в теле класса.

class Customer(name: String) {
    val customerKey = name.uppercase()
}

Компактная инициализация #

Для объявления и инициализации свойств основного конструктора в Kotlin есть лаконичное синтаксическое решение:

class Person(val firstName: String, val lastName: String, var age: Int)

Такие объявления также могут включать в себя значения свойств класса по умолчанию.

class Person(val firstName: String, val lastName: String, var isEmployed: Boolean = true)

Вы можете использовать завершающую запятую при объявлении свойств класса.

class Person(
    val firstName: String,
    val lastName: String,
    var age: Int, // завершающая запятая
) { /*...*/ }

Свойства, объявленные в основном конструкторе, могут быть изменяемыми (var) и неизменяемыми (val).

Методы класса #

Функции, объявленные в теле класса, называют методами. Внутри метода объект доступен через ключевое слово this. Параметр основного конструктора можно использовать в методе, только если он объявлен свойством (val или var):

class Rectangle(val width: Int, val height: Int) {
    fun area(): Int = width * height

    fun describe(): String = "Прямоугольник ${width}x${height}, площадь ${area()}"
}

fun main() {
    val rectangle = Rectangle(3, 4) // ключевого слова new в Kotlin нет
    println(rectangle.area())       // 12
    println(rectangle.describe())   // Прямоугольник 3x4, площадь 12
}

Экземпляр класса создаётся вызовом конструктора по имени класса.

Наследование #

Для всех классов в Kotlin родительским суперклассом является класс Any. Он также является родительским классом для любого класса, в котором не указан какой-либо другой родительский класс.

class Example // Неявно наследуется от Any

У Any есть три метода: equals(), hashCode() и toString(). Эти методы определены для всех классов в Kotlin

Разрешение наследования #

По умолчанию все классы в Kotlin имеют статус final, который блокирует возможность наследования. Чтобы сделать класс наследуемым, его нужно пометить ключевым словом open.

open class Base // Класс открыт для наследования

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

open class Base(p: Int)

class Derived(p: Int) : Base(p)

Если у класса есть основной конструктор, базовый тип может (и должен) быть проинициализирован там же, с использованием параметров основного конструктора.

Переопределение методов класса #

Kotlin требует явно указывать модификаторы и для членов, которые могут быть переопределены, и для самого переопределения.

open class Shape {
    open fun draw() { /*...*/ }
    fun fill() { /*...*/ }
}

class Circle : Shape() {
    override fun draw() { /*...*/ }
}

Для Circle.draw() необходим модификатор override. В случае её отсутствия компилятор выдаст ошибку. Если у функции типа Shape.fill() нет модификатора open, объявление метода с такой же сигнатурой в производном классе невозможно, с override или без. Если класс помечен как final, его члены нельзя объявить open.

Член класса, помеченный override, является сам по себе open, т.е. он может быть переопределён в производных классах. Если вы хотите запретить возможность переопределения такого члена, используйте final.

open class Rectangle : Shape() {
    final override fun draw() { /*...*/ }
}

Переопределение toString() #

Стандартная реализация toString(), унаследованная от Any, возвращает имя класса и служебное значение, например Rectangle@1b6d3586. Чтобы получать читаемое представление объекта, метод переопределяют:

class Point(val x: Int, val y: Int) {
    override fun toString(): String = "Point(x=$x, y=$y)"
}

fun main() {
    println(Point(1, 2))    // Point(x=1, y=2)
    println(Point(1, 2).toString())
}

Значения свойств подставляются в текст строковыми шаблонами. Функции println и строковые шаблоны вызывают toString() автоматически, поэтому переопределение меняет вывод во всём приложении.

Порядок инициализации производного класса #

При создании нового экземпляра класса в первую очередь выполняется инициализация базового класса (этому шагу предшествует только оценка аргументов, передаваемых в конструктор базового класса) и, таким образом, происходит до запуска логики инициализации производного класса.

open class Base(val name: String) {
    init { println("Инициализация класса Base") }
    open val size: Int = 
        name.length.also { println("Инициализация свойства size в классе Base: $it") }
}
class Derived(
    name: String,
    val lastName: String,
) : Base(name.replaceFirstChar { it.uppercase() }.also { println("Аргументы, переданные в конструктор класса Base: $it") }) {
    init { println("Инициализация класса Derived") }
    override val size: Int =
        (super.size + lastName.length).also { println("Инициализация свойства size в классе Derived: $it") }
}
fun main() {
    println("Построение класса Derived(\"hello\", \"world\")")
    Derived("hello", "world")
}

Вывод приложения:

Построение класса Derived("hello", "world")
Аргументы, переданные в конструктор класса Base: Hello
Инициализация класса Base
Инициализация свойства size в классе Base: 5
Инициализация класса Derived
Инициализация свойства size в классе Derived: 10

Вспомогательные объекты #

Объявление объекта внутри класса может быть отмечено ключевым словом companion.

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

Для вызова членов такого companion объекта используется имя класса.

val instance = MyClass.create()

Необязательно указывать имя вспомогательного объекта. Тогда он будет назван Companion.

class MyClass {
    companion object { }
}
val x = MyClass.Companion

Вспомогательный объект заменяет статические члены из других языков: в нём размещают константы и фабричные функции. Класс и его вспомогательный объект видят приватные члены друг друга:

class User private constructor(val login: String) {
    companion object {
        private const val MIN_LOGIN_LENGTH = 3

        fun create(login: String): User? =
            if (login.length >= MIN_LOGIN_LENGTH) User(login) else null
    }
}

fun main() {
    println(User.create("alice")?.login) // alice
    println(User.create("al"))           // null
}

Фабричный метод create обращается к приватному конструктору и к приватной константе MIN_LOGIN_LENGTH, хотя снаружи класса они недоступны.