Классы и наследование

Классы

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

class Invoice {
}

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

class Empty

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

Класс в Kotlin может иметь первичный конструктор (primary constructor) и один или более вторичных конструкторов (secondary constructors). Первичный конструктор является частью заголовка класса, его объявление идёт сразу после имени класса (и необязательных параметров):

class Person constructor(firstName: String)

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

class Person(firstName: String)

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

class Customer(name: String) {
    init {
        logger.info("Customer initialized with value ${name}")
    }
}

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

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

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

class Person(val firstName: String, val lastName: String, var age: Int) {
  // ...
}

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

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

class Customer public @Inject constructor(name: String) { ... }

Для более подробной информации по данному вопросу см. "Модификаторы доступа".

Второстепенные конструкторы

В классах также могут быть объявлены дополнительные конструкторы (secondary constructors), перед которыми используется ключевое слово constructor:

class Person {
    constructor(parent: Person) {
        parent.children.add(this)
    }
}

Если у класса есть главный (первичный) конструктор, каждый последующий конструктор должен прямо или косвенно ссылаться (через другой(ие) конструктор(ы)) на первичный:

class Person(val name: String) {
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

Если в абстрактном классе не объявлено никаких конструкторов (первичных или второстепенных), у этого класса автоматически сгенерируется пустой конструктор без параметров. Видимость этого конструктора будет public. Если вы не желаете иметь класс с открытым public конструктором, вам необходимо объявить пустой конструктор с соответствующим модификатором видимости:

class DontCreateMe private constructor () {
}

Примечание: В виртуальной машине JVM компилятор генерирует дополнительный конструктор без параметров в случае, если все параметры первичного конструктора имеют значения по умолчанию. Это делает использование таких библиотек, как Jackson и JPA, более простым в языке Kotlin, так как они используют пустые конструкторы при создании экземпляров классов.

class Customer(val customerName: String = "")

Создание экземпляров классов

Для создания экземпляра класса конструктор вызывается так, как если бы он был обычной функцией:

val invoice = Invoice()

val customer = Customer("Joe Smith")

Обращаем ваше внимание на то, что в Kotlin не используется ключевое слово new.

Члены класса

Классы могут содержать в себе:

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

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

class Example // Implicitly inherits from Any

Класс Any не является аналогом java.lang.Object. В частности, у него нет никаких членов кроме методов: equals(), hashCode(), и toString(). Пожалуйста, ознакомьтесь с совместимостью c Java для более подробной информации.

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

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) {
    }
}

Ключевое слово open является противоположностью слову final в Java: оно позволяет другим классам наследоваться от данного. По умолчанию, все классы в Kotlin имеют статус final, что отвечает Effective Java, Item 17: Design and document for inheritance or else prohibit it.

Переопределение членов класса

Как упоминалось ранее, мы придерживаемся идеи определённости и ясности в языке Kotlin. И, в отличие от Java, Kotlin требует чёткой аннотации и для членов, которые могут быть переопределены, и для самого переопределения:

open class Base {
  open fun v() {}
  fun nv() {}
}
class Derived() : Base() {
  override fun v() {}
}

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

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

open class AnotherDerived() : Base() {
  final override fun v() {}
}

Стойте! Как мне теперь хакнуть свои библиотеки?

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

Мы думаем, что это не является недостатком по следующим причинам:

  • Опыт поколений говорит о том, что, в любом случае, лучше не позволять внедрять такие хаки
  • Люди успешно используют другие языки (C++, C#), которые имеют аналогичных подход к этому вопросу
  • Если кто-то действительно хочет хакнуть, пусть напишет свой код на Java и вызовет его в Kotlin (см. Java-совместимость)

Правила переопределения

В Kotlin правила наследования имплементации определены следующим образом: если класс перенимает большое количество имплементаций одного и того члена от ближайших родительских классов, он должен переопределить этот член и обеспечить свою собственную имплементацию (возможно, используя одну из унаследованных). Для того, чтобы отметить супертип (родительский класс), от которого мы унаследовали данную имплементацию, мы используем ключевое слово super. Для уточнения имя родительского супертипа используются треугольные скобки, например super<Base>:

open class A {
  open fun f() { print("A") }
  fun a() { print("a") }
}

interface B {
  fun f() { print("B") } // interface members are 'open' by default
  fun b() { print("b") }
}

class C() : A(), B {
  // The compiler requires f() to be overridden:
  override fun f() {
    super<A>.f() // call to A.f()
    super<B>.f() // call to B.f()
  }
}

Нормально наследоваться одновременно от A и B. У нас не возникнет никаких проблем с a() и b() в том случае, если C унаследует только одну имплементацию этих функций. Но для f() у нас есть две имплементации, унаследованные классом C, поэтому необходимо переопределить f() в C и обеспечить нашу собственную реализацию этого метода для устранения получившейся неоднозначности.

Абстрактные классы

Класс и некоторые его члены могут быть объявлены как abstract. Абстрактный член не имеет реализации в его классе. Обратите внимание, что нам не надо аннотировать абстрактный класс или функцию словом open - это подразумевается и так.

Можно переопределить не-абстрактный open член абстрактным

open class Base {
  open fun f() {}
}

abstract class Derived : Base() {
  override abstract fun f()
}

Объекты-помощники

В Kotlin, в отличие от Java или C#, в классах не бывает статических методов. В большинстве случаев рекомендуется использовать функции на уровне пакета (ориг.: "package-level functions").

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

В частности, если вы объявляете объект-помощник в своём классе, у вас появляется возможность обращаться к его членам, используя тот же синтаксис, как при использовании статических методов в Java/C# (указав название класса для доступа).

Прочие классы

Также обратите внимание на: