Классы и наследование
Классы
Классы в Kotlin объявляются с использованием ключевого слова class:
class Invoice { /*...*/ }
Объявление класса состоит из имени класса, заголовка класса (указывающего его параметры типа, первичный конструктор и т.д.) и тела класса, заключенного в фигурные скобки. И заголовок, и тело являются необязательными; если у класса нет тела, фигурные скобки можно опустить.
class Empty
Конструкторы
Класс в Kotlin может иметь первичный конструктор и один или несколько вторичных конструкторов. Первичный конструктор является частью заголовка класса: он следует за именем класса (и необязательными параметрами типа).
class Person constructor(firstName: String) { /*...*/ }
Если первичный конструктор не содержит анотаций или модификаторов видимости, ключевое слово constructor можно опустить:
class Person(firstName: String) { /*...*/ }
Первичный конструктор не может содержать никакого кода. Код инициализации можно разместить в блоках инициализации, которые предваряются ключевым словом init.
Во время инициализации экземпляра блоки инициализации выполняются в том же порядке, что и в теле класса, чередуясь с инициализаторами свойств:
//sampleStart
class InitOrderDemo(name: String) {
val firstProperty = "First property: $name".also(::println)
init {
println("First initializer block that prints ${name}")
}
val secondProperty = "Second property: ${name.length}".also(::println)
init {
println("Second initializer block that prints ${name.length}")
}
}
//sampleEnd
fun main() {
InitOrderDemo("hello")
}
Обратите внимание, что параметры первичного конструктора могут использоваться в блоках инициализации. Они также могут использоваться в инициализаторах свойств, объявленных в теле класса:
class Customer(name: String) {
val customerKey = name.toUpperCase()
}
На самом деле, для объявления свойств и их инициализации из первичного конструктора Kotlin имеет краткую синтаксическую конструкцию:
class Person(val firstName: String, val lastName: String, var age: Int) { /*...*/ }
Вы можете использовать запятую в конце при объявлении свойств класса:
class Person(
val firstName: String,
val lastName: String,
var age: Int, // trailing comma
) { /*...*/ }
Подобно обычным свойствам, свойства, объявленные в первичном конструкторе, могут быть изменяемыми (var) или неизменяемыми (val).
Если конструктор имеет анотации или модификаторы видимости, ключевое слово constructor требуется, а модификаторы ставятся перед ним:
class Customer public @Inject constructor(name: String) { /*...*/ }
Для получения более подробной информации см. Модификаторы видимости.
Вторичные конструкторы
Класс также может объявлять вторичные конструкторы, которые предваряются ключевым словом constructor:
class Person {
var children: MutableList<Person> = mutableListOf<>()
constructor(parent: Person) {
parent.children.add(this)
}
}
Если у класса есть первичный конструктор, каждый вторичный конструктор должен делегировать первичному конструктору, либо напрямую, либо косвенно через другие вторичные конструкторы. Делегирование другому конструктору того же класса выполняется с использованием ключевого слова this:
class Person(val name: String) {
var children: MutableList<Person> = mutableListOf<>()
constructor(name: String, parent: Person) : this(name) {
parent.children.add(this)
}
}
Обратите внимание, что код в блоках инициализации фактически становится частью первичного конструктора. Делегирование первичному конструктору происходит как первое утверждение вторичного конструктора, поэтому код во всех блоках инициализации и инициализаторах свойств выполняется перед телом вторичного конструктора. Даже если у класса нет первичного конструктора, делегирование все равно происходит неявно, а блоки инициализации все еще выполняются:
//sampleStart
class Constructors {
init {
println("Init block")
}
constructor(i: Int) {
println("Constructor")
}
}
//sampleEnd
fun main() {
Constructors(1)
}
Если не абстрактный класс не объявляет никаких конструкторов (первичных или вторичных), у него будет сгенерированный первичный конструктор без аргументов. Видимость конструктора будет public. Если вы не хотите, чтобы у вашего класса был public конструктор, вам нужно объявить пустой первичный конструктор с нестандартной видимостью:
class DontCreateMe private constructor () { /*...*/ }
ПРИМЕЧАНИЕ: В JVM, если у всех параметров первичного конструктора есть значения по умолчанию, компилятор сгенерирует дополнительный конструктор без параметров, который будет использовать значения по умолчанию. Это упрощает использование Kotlin с библиотеками, такими как Jackson или JPA, которые создают экземпляры классов с помощью конструкторов без параметров.
class Customer(val customerName: String = "")
Создание экземпляров классов
Для создания экземпляра класса мы вызываем конструктор, как если бы это была обычная функция:
val invoice = Invoice()
val customer = Customer("Joe Smith")
Обратите внимание, что в Kotlin нет ключевого слова new.
Создание экземпляров вложенных, внутренних и анонимных внутренних классов описано в Вложенных классах.
Члены класса
Классы могут содержать:
- Конструкторы и блоки инициализации
- Функции
- Свойства
- Вложенные и внутренние классы
- Объявления объектов
Наследование
Все классы в Kotlin имеют общий суперкласс Any, который является по умолчанию суперклассом для класса без объявленных суперобъектов:
class Example // Implicitly inherits from Any
Any имеет три метода: equals(), hashCode() и toString(). Таким образом, они определены для всех классов Kotlin.
По умолчанию классы Kotlin являются final: они не могут наследоваться. Чтобы сделать класс наследуемым, пометьте его ключевым словом open.
open class Base //Class is open for inheritance
Для объявления явного супертипа поместите тип после двоеточия в заголовке класса:
open class Base(p: Int) class Derived(p: Int) : Base(p)
Если у производного класса есть первичный конструктор, базовый класс может (и должен) быть инициализирован прямо там, используя параметры первичного конструктора.
Если у производного класса нет первичного конструктора, каждый вторичный конструктор должен инициализировать базовый тип с использованием ключевого слова super или делегировать другому конструктору, который это делает. Обратите внимание, что в этом случае разные вторичные конструкторы могут вызывать разные конструкторы базового типа:
class MyView : View {
constructor(ctx: Context) : super(ctx)
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
Переопределение методов
Как мы уже упоминали, в Kotlin мы придерживаемся явности. Поэтому Kotlin требует явных модификаторов для переопределяемых членов (мы называем их open) и для переопределений:
open class Shape {
open fun draw() { /*...*/ }
fun fill() { /*...*/ }
}
class Circle() : Shape() {
override fun draw() { /*...*/ }
}
Модификатор override требуется для Circle.draw(). Если бы его не было, компилятор бы пожаловался. Если нет модификатора open на функции, как Shape.fill(), объявление метода с той же сигнатурой в подклассе является незаконным, как с override, так и без него. Модификатор open не имеет эффекта, когда добавлен к членам финального класса (т.е. класса без модификатора open).
Член, помеченный override, сам по себе open, т.е. может быть переопределён в подклассах. Если вы хотите запретить повторное переопределение, используйте final:
open class Rectangle() : Shape() {
final override fun draw() { /*...*/ }
}
Переопределение свойств
Переопределение свойств работает аналогично переопределению методов; свойства, объявленные в суперклассе, которые затем переобъявляются в производном классе, должны быть снабжены префиксом override и должны иметь совместимый тип. Каждое объявленное свойство может быть переопределено свойством с инициализатором или свойством с методом get.
open class Shape {
open val vertexCount: Int = 0
}
class Rectangle : Shape() {
override val vertexCount = 4
}
Вы также можете переопределить свойство val свойством var, но не наоборот. Это разрешено, потому что свойство val по сути объявляет метод get, а переопределение его как var дополнительно объявляет метод set в производном классе.
Обратите внимание, что ключевое слово override можно использовать в объявлении свойства в первичном конструкторе.
interface Shape {
val vertexCount: Int
}
class Rectangle(override val vertexCount: Int = 4) : Shape // Always has 4 vertices
class Polygon : Shape {
override var vertexCount: Int = 0 // Can be set to any number later
}
Порядок инициализации производного класса
Во время создания нового экземпляра производного класса инициализация базового класса выполняется как первый шаг (предваряемый только вычислением аргументов для конструктора базового класса) и, следовательно, происходит до запуска логики инициализации производного класса.
//sampleStart
open class Base(val name: String) {
init { println("Initializing Base") }
open val size: Int =
name.length.also { println("Initializing size in Base: $it") }
}
class Derived(
name: String,
val lastName: String,
) : Base(name.capitalize().also { println("Argument for Base: $it") }) {
init { println("Initializing Derived") }
override val size: Int =
(super.size + lastName.length).also { println("Initializing size in Derived: $it") }
}
//sampleEnd
fun main() {
println("Constructing Derived(\"hello\", \"world\")")
val d = Derived("hello", "world")
}
Это означает, что к моменту выполнения конструктора базового класса свойства, объявленные или переопределенные в производном классе, еще не инициализированы. Если какие-либо из этих свойств используются в логике инициализации базового класса (прямо или косвенно, через другой переопределенный член open), это может привести к некорректному поведению или ошибке во время выполнения. При проектировании базового класса, поэтому следует избегать использования членов open в конструкторах, инициализаторах свойств и блоках init.
Вызов реализации суперкласса
Код в производном классе может вызывать функции и реализации обращений к свойствам суперкласса, используя ключевое слово super:
open class Rectangle {
open fun draw() { println("Drawing a rectangle") }
val borderColor: String get() = "black"
}
class FilledRectangle : Rectangle() {
override fun draw() {
super.draw()
println("Filling the rectangle")
}
val fillColor: String get() = super.borderColor
}
Внутри вложенного класса доступ к суперклассу внешнего класса выполняется с помощью ключевого слова super, квалифицированного именем внешнего класса: super@Outer:
class FilledRectangle: Rectangle() {
fun draw() { /* ... */ }
val borderColor: String get() = "black"
inner class Filler {
fun fill() { /* ... */ }
fun drawAndFill() {
super@FilledRectangle.draw() // Calls Rectangle's implementation of draw()
fill()
println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // Uses Rectangle's implementation of borderColor's get()
}
}
}
Правила переопределения
В Kotlin, наследование реализации регулируется следующим правилом: если класс наследует несколько реализаций одного и того же члена от своих непосредственных суперклассов, он должен переопределить этот член и предоставить собственную реализацию (возможно, используя одну из унаследованных). Для обозначения супертипа, из которого взята унаследованная реализация, мы используем super, квалифицированный именем супертипа в угловых скобках, например super<Base>.
open class Rectangle {
open fun draw() { /* ... */ }
}
interface Polygon {
fun draw() { /* ... */ } // interface members are 'open' by default
}
class Square() : Rectangle(), Polygon {
// The compiler requires draw() to be overridden:
override fun draw() {
super<Rectangle>.draw() // call to Rectangle.draw()
super<Polygon>.draw() // call to Polygon.draw()
}
}
В порядке вещей наследование от Rectangle и Polygon, но оба из них имеют свои реализации draw(), поэтому нам нужно переопределить draw() в Square и предоставить собственную реализацию, которая устранит неоднозначность.
Абстрактные классы
Класс и некоторые его члены могут быть объявлены abstract. Абстрактный член не имеет реализации в своём классе. Обратите внимание, что нам не нужно аннотировать абстрактный класс или функцию с open – это само собой разумеется.
Можно переопределить не абстрактный открытый член абстрактным
open class Polygon {
open fun draw() {}
}
abstract class Rectangle : Polygon() {
abstract override fun draw()
}
Компаньон-объекты
Если вам нужно написать функцию, которую можно вызвать без экземпляра класса, но которая нуждается в доступе к внутренностям класса (например, фабричный метод), вы можете написать её как член объявления объекта внутри этого класса.
Более конкретно, если вы объявляете компаньон-объект внутри своего класса, вы можете получить доступ к его членам, используя только имя класса в качестве квалификатора.
© 2010–2020 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/reference/classes.html