Skip to content

Latest commit

 

History

History
1544 lines (1114 loc) · 79.2 KB

l2.adoc

File metadata and controls

1544 lines (1114 loc) · 79.2 KB

2. ООП

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

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

  • Модификаторы видимости

    • Primary

    • Secondary

  • Properties

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

  • Интерфейсы

  • Data классы

  • Sealed классы

  • Object

  • Делегирование: классы и property

  • Enums

  • Companion object

Мы предполагаем что слушатель курса уже знает основы ООП, и в лекции будут рассказаны особенности ООП в Kotlin.

Note
Важные отличия: В Kotlin в отличии от Java нету статических методов и переменных, для объявления статических данных использоваются companion object.

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

Классы

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

class Invoice {
}

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

class Empty

<a name="constructors"></a>

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

Класс в <b>Kotlin</b> может иметь первичный конструктор (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()
}

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

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 компилятор генерирует дополнительный конструктор без параметров в случае, если все параметры первичного конструктора имеют значения по умолчанию. Это делает использование таких библиотек, как <b>Jackson</b> и <b>JPA</b>, более простым в языке <b>Kotlin</b>, так как они используют пустые конструкторы при создании экземпляров классов.

class Customer(val customerName: String = "")
Создание экземпляров классов

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

val invoice = Invoice()

val customer = Customer("Joe Smith")

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

Члены класса

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

<a name="inheritance"></a>

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

Для всех классов в языке <b>Koltin</b> родительским суперклассом является класс 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 в <b>Java</b>: оно позволяет другим классам наследоваться от данного. По умолчанию, все классы в <b>Kotlin</b> имеют статус final, что отвечает Effective Java, Item 17: Design and document for inheritance or else prohibit it.

<a name="overriding-properties"></a>

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

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

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) будет сложно унаследоваться от чего-нибудь внутри используемых вами библиотек для того, чтобы переопределить не предназначенный для этого метод и внедрить туда свой гнусный хак.

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

  • Опыт поколений говорит о том, что, в любом случае, лучше не позволять внедрять такие хаки

  • Люди успешно используют другие языки (<b>C++</b>, <b>C#</b>), которые имеют аналогичный подход к этому вопросу

  • Если кто-то действительно хочет хакнуть, пусть напишет свой код на <b>Java</b> и вызовет его в <b>Kotlin</b> (см. Java-совместимость)

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

В <b>Kotlin</b> правила наследования имплементации определены следующим образом: если класс перенимает большое количество имплементаций одного и того члена от ближайших родительских классов, он должен переопределить этот член и обеспечить свою собственную имплементацию (возможно, используя одну из унаследованных). Для того, чтобы отметить супертип (родительский класс), от которого мы унаследовали данную имплементацию, мы используем ключевое слово 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()
}

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

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

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

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


Классы данных

Нередко мы создаём классы, единственным назначением которых является хранение данных. Функционал таких классов зависит от самих данных, которые в них хранятся. В <b>Kotlin</b> класс может быть отмечен словом data:

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

Такой класс называется классом данных. Компилятор автоматически извлекает все члены данного класса из свойств, объявленных в первичном конструкторе:

  • пара функций equals()/hashCode(),

  • toString() в форме "User(name=John, age=42)",

  • функции componentN(), которые соответствуют свойствам, в зависимости от их порядка либо объявления,

  • функция copy() (см. ниже)

Если какая-либо из этих функций явно определена в теле класса (или унаследована от родительского класса), то генерироваться она не будет.

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

  • Первичный конструктор должен иметь как минимум один параметр;

  • Все параметры первичного конструктора должны быть отмечены, как val или var;

  • Классы данных не могут быть абстрактными, open, sealed или inner;

  • Дата-классы не могут наследоваться от других классов (но могут реализовывать интерфейсы).

Начиная с версии 1.1, классы данных могут расширять другие классы (см. примеры в Sealed classes)

Для того, чтобы у сгенерированного в JVM класса был конструктор без параметров, значения всех свойств должны быть заданы по умолчанию (см. Конструкторы) `kotlin data class User(val name: String = "", val age: Int = 0) `

Копирование

Довольно часто нам приходится копировать объект с изменением только некоторых его свойств. Для этой задачи генерируется функция copy(). Для написанного выше класса User такая реализация будет выглядеть следующим образом:

fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

Это позволяет нам писать

val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)

Классы данных и мульти-декларации

Сгенерированные для классов данных составные функции позволяют использовать их в мульти-декларациях:

val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age") // выводит "Jane, 35 years of age"

Стандартные классы данных

Стандартная библиотека предоставляет Pair и Triple. Однако, в большинстве случаев, проименованные классы данных являются лучшим решением, потому что делают код более читаемым, избегая малосодержательные имена для свойств.

——————————————————————————– <!--# Delegated Properties-→ <!--https://habrahabr.ru/company/JetBrains/blog/183444/-

Делегированные свойства

За помощь в переводе спасибо официальному блогу JetBrains на Хабрахабре

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

  • ленивые свойства (lazy properties): значение вычисляется один раз, при первом обращении

  • свойства, на события об изменении которых можно подписаться (observable properties)

  • свойства, хранимые в ассоциативном списке, а не в отдельных полях

Для таких случаев, Kotlin поддерживает делегированные свойства:

class Example {
    var p: String by Delegate()
}

Их синтаксис выглядит следующим образом: val/var <имя свойства>: <Тип> by <выражение>. Выражение после byделегат: обращения (get(), set()) к свойству будут обрабатываться этим выражением. Делегат не обязан реализовывать какой-то интерфейс, достаточно, чтобы у него были методы getValue() и setValue() с определённой сигнатурой:

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$thisRef, спасибо за делегирование мне '${property.name}'!"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("$value было присвоено значению '${property.name} в $thisRef.'")
    }
}

Когда мы читаем значение свойства p, вызывается метод getValue() класса Delegate, причем первым параметром ей передается тот объект, у которого запрашивается свойство p, а вторым — объект-описание самого свойства p (у него можно, в частности, узнать имя свойства). Например:

val e = Example()
println(e.p)

Этот код выведет

Example@33a17727, спасибо за делегирование мне ‘p’!

Похожим образом, когда мы обращаемся к p, вызывается метод setValue(). Два первых параметра — такие же, как у get(), а третий — присваиваемое значение свойства:

e.p = "NEW"

Этот код выведет

NEW было присвоено значению ‘p’ в Example@33a17727.

Спецификация требований к делегированным свойствам может быть найдена ниже.

Заметьте, что начиная с версии Kotlin 1.1, вы можете объявлять делегированные свойства внутри функций или блоков кода, а не только внутри классов. Снизу вы можете найти пример.

Стандартные делегаты

Стандартная библиотека Kotlin предоставляет несколько полезных видов делегатов:

Ленивые свойства (lazy properties)

lazy() это функция, которая принимает лямбду и возвращает экземпляр класса Lazy<T>, который служит делегатом для реализации ленивого свойства: первый вызов get() запускает лямбда-выражение, переданное lazy() в качестве аргумента, и запоминает полученное значение, а последующие вызовы просто возвращают вычисленное значение.

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main(args: Array<String>) {
    println(lazyValue)
    println(lazyValue)
}

Этот код выведет:

computed!
Hello
Hello

По умолчанию вычисление ленивых свойств синхронизировано: значение вычисляется только в одном потоке выполнения, и все остальные потоки могут видеть одно и то же значение. Если синхронизация не требуется, передайте LazyThreadSafetyMode.PUBLICATION в качестве параметра в функцию lazy(), тогда несколько потоков смогут исполнять вычисление одновременно. Или если вы уверены, что инициализация всегда будет происходить в одном потоке исполнения, вы можете использовать режим LazyThreadSafetyMode.NONE, который не гарантирует никакой потокобезопасности.

Observable свойства

Функция Delegates.observable() принимает два аргумента: начальное значение свойства и обработчик (лямбда), который вызывается при изменении свойства. У обработчика три параметра: описание свойства, которое изменяется, старое значение и новое значение.

import kotlin.properties.Delegates

class User {
    var name: String by Delegates.observable("<no name>") {
        prop, old, new ->
        println("$old -> $new")
    }
}

fun main(args: Array<String>) {
    val user = User()
    user.name = "first"
    user.name = "second"
}

Этот код выведет:

<no name> -> first
first -> second

Если Вам нужно иметь возможность запретить присваивание некоторых значений, используйте функцию vetoable() вместо observable().

Хранение свойств в ассоциативном списке

Один из самых частых сценариев использования делегированных свойств заключается в хранении свойств в ассоциативном списке. Это полезно в "динамическом" коде, например, при работе с JSON:

class User(val map: Map<String, Any?>) {
    val name: String by map
    val age: Int     by map
}

В этом примере конструктор принимает ассоциативный список

val user = User(mapOf(
    "name" to "John Doe",
    "age"  to 25
))

Делегированные свойства берут значения из этого ассоциативного списка (по строковым ключам)

println(user.name) // Prints "John Doe"
println(user.age)  // Prints 25

Также, если вы используете MutableMap вместо Map, поддерживаются изменяемые свойства (var):

class MutableUser(val map: MutableMap<String, Any?>) {
    var name: String by map
    var age: Int     by map
}

Локальные делегированные свойства (с версии 1.1)

Вы можете объявить локальные переменные как делегированные свойства. Например, вы можете сделать локальную переменную ленивой:

fun example(computeFoo: () -> Foo) {
    val memoizedFoo by lazy(computeFoo)

    if (someCondition && memoizedFoo.isValid()) {
        memoizedFoo.doSomething()
    }
}

Переменная memoizedFoo будет вычислена только при первом обращении к ней. Если условие someCondition будет ложно, значение переменной не будет вычислено вовсе.

<a name="property-delegate-requirements"></a>

Требования к делегированным свойствам

Здесь приведены требования к объектам-делегатам.

Для read-only свойства (например val), делегат должен предоставлять функцию getValue, которая принимает следующие параметры:

  • thisRef — должен иметь такой же тип или быть наследником типа хозяина свойства (для расширений — тип, который расширяется)

  • property — должен быть типа KProperty<*> или его родительского типа. Эта функция должна возвращать значение того же типа, что и свойство (или его родительского типа).

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

  • thisRef — то же что и у getValue(),

  • property — то же что и у getValue(),

  • new value — должен быть того же типа, что и свойство (или его родительского типа).

Функции getValue() и/или setValue() могут быть предоставлены либо как члены класса-делегата, либо как его расширения. Последнее полезно когда вам нужно делегировать свойство объекту, который изначально не имеет этих функций. Обе эти функции должны быть отмечены с помощью ключевого слова operator.

Эти интерфейсы объявлены в стандартной библиотеке Kotlin:

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}

interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}
Translation Rules

Для каждого делегированного свойства компилятор Kotlin "за кулисами" генерирует вспомогательное свойство и делегирует его. Например, для свойства prop генерируется скрытое свойство prop$delegate, и исполнение геттеров и сеттеров просто делегируется этому дополнительному свойству:

class C {
    var prop: Type by MyDelegate()
}

// этот код генерируется компилятором:
class C {
    private val prop$delegate = MyDelegate()
    var prop: Type
        get() = prop$delegate.getValue(this, this::prop)
        set(value: Type) = prop$delegate.setValue(this, this::prop, value)
}

Компилятор Kotlin предоставляет всю необходимую информацию о prop в аргументах: первый аргумент this ссылается на экземпляр внешнего класса C и this::prop reflection-объект типа KProperty, описывающий сам prop.

Заметьте, что синтаксис this::prop для обращения к bound callable reference напрямую в коде программы доступен только с Kotlin версии 1.1

Предоставление делегата

Примечание: Предоставление делегата доступно в Kotlin начиная с версии 1.1

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

Один из возможных юзкейсов provideDelegate — это проверка состояния свойства при его создании.

Например, если вы хотите проверить имя свойства перед связыванием, вы можете написать что-то вроде:

class ResourceLoader<T>(id: ResourceID<T>) {
    operator fun provideDelegate(
            thisRef: MyUI,
            prop: KProperty<*>
    ): ReadOnlyProperty<MyUI, T> {
        checkProperty(thisRef, prop.name)
        // создание делегата
    }

    private fun checkProperty(thisRef: MyUI, name: String) { ... }
}

fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { ... }

class MyUI {
    val image by bindResource(ResourceID.image_id)
    val text by bindResource(ResourceID.text_id)
}

provideDelegate имеет те же параметры, что и getValue:

  • thisRef — должен иметь такой же тип, или быть наследником типа хозяина свойства (для расширений — тип, который расширяется)

  • property — должен быть типа KProperty<*> или его родительского типа. Эта функция должна возвращать значение того же типа, что и свойство (или его родительского типа)

Метод provideDelegate вызывается для каждого свойства во время создания экземпляра MyUI, и сразу совершает необходимые проверки.

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

// Проверяем имя свойства без "provideDelegate"
class MyUI {
    val image by bindResource(ResourceID.image_id, "image")
    val text by bindResource(ResourceID.text_id, "text")
}

fun <T> MyUI.bindResource(
        id: ResourceID<T>,
        propertyName: String
): ReadOnlyProperty<MyUI, T> {
   checkProperty(this, propertyName)
   // создание делегата
}

В сгенерированном коде метод provideDelegate вызывается для инициализации вспомогательного свойства prop$delegate. Сравните сгенерированный для объявления свойства код val prop: Type by MyDelegate() со сгенерированным кодом из Transaction Rules (когда provideDelegate не представлен):

class C {
    var prop: Type by MyDelegate()
}

// этот код будет сгенерирован компилятором
// когда функция 'provideDelegate' доступна:
class C {
    // вызываем "provideDelegate" для создания вспомогательного свойства "delegate"
    private val prop$delegate = MyDelegate().provideDelegate(this, this::prop)
    val prop: Type
        get() = prop$delegate.getValue(this, this::prop)
}

Заметьте, что метод provideDelegate влияет только на создание вспомогательного свойства и не влияет на код, генерируемый геттером или сеттером.


Делегирование

Делегирование класса

Шаблон делегирования является хорошей альтернативой наследованию, и Kotlin поддерживает его нативно, освобождая вас от необходимости написания шаблонного кода.

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
    val b = BaseImpl(10)
    Derived(b).print() // prints 10
}

Ключевое слово by в оглавлении Derived, находящееся после типа делегируемого класса, говорит о том, что объект b типа Base будет храниться внутри экземпляра Derived, и компилятор сгенерирует у Derived соответствующие методы из Base, которые при вызове будут переданы объекту b


Перечисляемые типы

Наиболее базовый пример использования enum — это реализация типобезопасных перечислений

enum class Direction {
    NORTH, SOUTH, WEST, EAST
}

Каждая enum-константа является объектом. При объявлении константы разделяются запятыми.

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

Так как константы являются экземплярами enum-класса, они могут быть инициализированы

enum class Color(val rgb: Int) {
        RED(0xFF0000),
        GREEN(0x00FF00),
        BLUE(0x0000FF)
}

Анонимные классы

Enum-константы также могут объявлять свои собственные анонимные классы

enum class ProtocolState {
    WAITING {
        override fun signal() = TALKING
    },

    TALKING {
        override fun signal() = WAITING
    };

    abstract fun signal(): ProtocolState
}

как с их собственными методами, так и с перегруженными методами базового класса. Следует заметить, что при объявлении в enum-классе каких-либо членов, необходимо отделять их от списка констант точкой с запятой, так же как и в Java.

Работа с enum-константами

Так же как и в Java, enum-классы в Kotlin имеют стандартные методы для вывода списка объявленных констант и для получения enum-константы по её имени. Ниже приведены сигнатуры этих методов:

EnumClass.valueOf(value: String): EnumClass
EnumClass.values(): Array<EnumClass>

Метод valueOf() выбрасывает исключение IllegalArgumentException, если указанное имя не соответствует ни одной константе, объявленной в классе.

Каждая enum-константа имеет поля, в которых содержатся её имя и порядковый номер в enum-классе:

val name: String
val ordinal: Int

Также enum-константы реализуют интерфейс Comparable. Порядок сортировки соответствует порядку объявления.


Интерфейсы

Интерфейсы в <b>Kotlin</b> очень похожи на интерфейсы в <b>Java 8</b>. Они могут содержать абстрактные методы, методы с реализацией. Главное отличие интерфейсов от абстрактных классов заключается в невозможности хранения переменных экземпляров. Они могут иметь свойства, но те должны быть либо абстрактными, либо предоставлять реализацию методов доступа.

Интерфейс определяется ключевым словом interface:

interface MyInterface {
    fun bar()
    fun foo() {
      // необязательное тело
    }
}

Реализация интерфейсов

Класс или объект могут реализовать любое количество интерфейсов:

class Child : MyInterface {
    override fun bar() {
        // тело
    }
}

Свойства в интерфейсах

Вы можете объявлять свойства в интерфейсах. Свойство, объявленное в интерфейсе, может быть либо абстрактным, либо иметь свою реализацию методов доступа. Свойства в интерфейсах не могут иметь backing fields, соответственно, методы доступа к таким свойствам не могут обращаться к backing fields.

interface MyInterface {
    val prop: Int // абстрактное свойство

    val propertyWithImplementation: String
        get() = "foo"

    fun foo() {
        print(prop)
    }
}

class Child : MyInterface {
    override val prop: Int = 29
}

Устранение противоречий при переопределении

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

interface A {
    fun foo() { print("A") }
    fun bar()
}

interface B {
    fun foo() { print("B") }
    fun bar() { print("bar") }
}

class C : A {
    override fun bar() { print("bar") }
}

class D : A, B {
    override fun foo() {
        super<A>.foo()
        super<B>.foo()
    }

    override fun bar() {
        super<B>.bar()
    }
}

Оба интерфейса A и B объявляют функции foo() и bar(). Оба реализуют foo(), но только B содержит реализацию bar() (bar() не отмечен как абстрактный метод в интерфейсе A, потому что в интерфейсах это подразумевается по умолчанию, если у функции нет тела). Теперь, если мы унаследуем какой-нибудь класс C от A, нам, очевидно, придётся переопределять bar(), обеспечивать его реализацию.

Однако если мы унаследуем D от A и B, нам надо будет переопределять все методы, которые мы унаследовали от этих интерфейсов. Это правило касается как тех методов, у которых имеется только одна реализация (bar()), так и тех, у которых есть несколько реализаций (foo()).

——————————————————————————– <!--Nested Classes-→

Вложенные классы

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

class Outer {
    private val bar: Int = 1
    class Nested {
        fun foo() = 2
    }
}

val demo = Outer.Nested().foo() // == 2

Внутренние классы

Класс может быть отмечен как внутренний с помощью слова inner, тем самым он будет иметь доступ к членам внешнего класса. Внутренние классы содержат ссылку на объект внешнего класса:

class Outer {
    private val bar: Int = 1
    inner class Inner {
        fun foo() = bar
    }
}

val demo = Outer().Inner().foo() // == 1

Подробнее об использовании this во внутренних классах: Qualified this expressions

Анонимные внутренние классы

Анонимные внутренние экземпляры классов создаются с помощью object expression:

window.addMouseListener(object: MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
        // ...
    }

    override fun mouseEntered(e: MouseEvent) {
        // ...
    }
})

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

val listener = ActionListener { println("clicked") }

Анонимные объекты и объявление объектов

Иногда нам необходимо получить экземпляр некоторого класса с незначительной модификацией, желательно без написания нового подкласса. <b>Java</b> справляется с этим с помощью вложенных анонимных классов. <b>Kotlin</b> несколько улучшает данный подход.

<a name="object-expressions"></a>

Анонимные объекты (ориг.:_Object expressions_)

Для того, чтобы создать объект анонимного класса, который наследуется от какого-то типа (типов), используется конструкция:

window.addMouseListener(object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
        // ...
    }

    override fun mouseEntered(e: MouseEvent) {
        // ...
    }
})

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

open class A(x: Int) {
    public open val y: Int = x
}

interface B {...}

val ab: A = object : A(1), B {
    override val y = 15
}

Если всё-таки нам нужен просто объект без всяких там родительских классов, то можем указать:

val adHoc = object {
    var x: Int = 0
    var y: Int = 0
}
print(adHoc.x + adHoc.y)

Код внутри объявленного объекта может обращаться к переменным за скобками так же, как вложенные анонимные классы в <b>Java</b>

fun countClicks(window: JComponent) {
    var clickCount = 0
    var enterCount = 0

    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }

        override fun mouseEntered(e: MouseEvent) {
            enterCount++
        }
    })
    // ...
}

<a name="object-declarations"></a>

Объявления объектов (ориг.:_Object declarations_)

[Синглтон](https://ru.wikipedia.org/wiki/%D0%9E%D0%B4%D0%B8%D0%BD%D0%BE%D1%87%D0%BA%D0%B0_(%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F)) - очень полезный паттерн программирования, и <b>Kotlin</b> (переняв у <b>Scala</b>) позволяет объявлять его довольно простым способом :

object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ...
    }

    val allDataProviders: Collection<DataProvider>
        get() = // ...
}

Это называется объявлением объекта и всегда имеет приставку в виде ключевого слова object. Аналогично объявлению переменной, объявление объекта не является выражением и не может быть использовано в правой части оператора присваивания.

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

DataProviderManager.registerDataProvider(...)

Подобные объекты могут иметь супертипы:

object DefaultListener : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
        // ...
    }

    override fun mouseEntered(e: MouseEvent) {
        // ...
    }
}

ПРИМЕЧАНИЕ: объявление объекта не может иметь локальный характер (т.е. быть вложенным непосредственно в функцию), но может быть вложено в объявление другого объекта или какого-либо невложенного класса.

<a name="companion-objects"></a>

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

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

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

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

val instance = MyClass.create()

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

class MyClass {
    companion object {
    }
}

val x = MyClass.Companion

Такие члены вспомогательных объектов выглядят, как статические члены в других языках программирования. На самом же деле, они являются членами реальных объектов и могут реализовывать, к примеру, интерфейсы:

interface Factory<T> {
    fun create(): T
}

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

Однако в <b>JVM</b> вы можете статически генерировать методы вспомогательных объектов и полей, используя аннотацию @JvmStatic@. См. Совместимость с Java.

Семантическое различие между анонимным объектом и декларируемым объектом.

Существует только одно смысловое различие между этими двумя понятиями:

  • анонимный объект инициализируется сразу после того, как был использован

  • декларированный объект инициализируется лениво, в момент первого к нему доступа

  • вспомогательный объект инициализируется в момент, когда класс, к которому он относится, загружен и семантически совпадает со статическим инициализатором <b>Java</b> '''

Свойства и поля

Объявление свойств

Классы в <b>Kotlin</b> могут иметь свойства: изменяемые (mutable) и неизменяемые (read-only) — var и val соответственно.

public class Address {
    public var name: String = ...
    public var street: String = ...
    public var city: String = ...
    public var state: String? = ...
    public var zip: String = ...
}

Для того, чтобы воспользоваться свойством, мы просто обращаемся к его имени (как в <b>Java</b>):

fun copyAddress(address: Address): Address {
    val result = Address() // нет никакого слова `new`
    result.name = address.name // вызов методов доступа
    result.street = address.street
    // ...
    return result
}

Геттеры и сеттеры

Полный синтаксис объявления свойства выглядит так:

var <propertyName>: <PropertyType> [= <property_initializer>]
    [<getter>]
    [<setter>]

Инициализатор property_initializer, геттер и сеттер можно не указывать. Также необязательно указывать тип свойства, если он может быть выведен из контекста или наследован от базового класса.

Примеры:

var allByDefault: Int? // ошибка: необходима явная инициализация, предусмотрены стандартные геттер и сеттер
var initialized = 1 // имеет тип Int, стандартный геттер и сеттер

Синтаксис объявления констант имеет два отличия от синтаксиса объявления изменяемых переменных: во-первых, объявление начинается с ключевого слова val вместо var, а во-вторых, объявление сеттера запрещено:

val simple: Int? // имеет тип Int, стандартный геттер, должен быть инициализирован в конструкторе
val inferredType = 1 // имеет тип Int и стандартный геттер

Мы можем самостоятельно описать методы доступа, как и обычные функции, прямо при объявлении свойств. Например, пользовательский геттер:

val isEmpty: Boolean
    get() = this.size == 0

Пользовательский сеттер выглядит примерно так:

var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value) // парсит строку и устанавливает значения для других свойств
    }

По договорённости, имя параметра сеттера - value, но вы можете использовать любое другое.

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

var setterVisibility: String = "abc"
    private set // сеттер имеет private доступ и стандартную реализацию

var setterWithAnnotation: Any? = null
    @Inject set // аннотирование сеттера с помощью Inject

<a name="backing-fields"></a>

Backing Fields

Классы в <b>Kotlin</b> не могут иметь полей. Т.е. переменные, которые вы объявляете внутри класса только выглядят и ведут себя как поля из Java, хотя на самом деле являются свойствами, т.к. для них неявно реализуются методы get и set. А сама переменная, в которой находится значение свойства, называется backing field. Однако, иногда, при использовании пользовательских методов доступа, необходимо иметь доступ к backing field. Для этих целей <b>Kotlin</b> предоставляет автоматическое backing field, к которому можно обратиться с помощью идентификатора field:

var counter = 0
    set(value) {
        if (value >= 0) field = value // значение при инициализации записывается прямиком в backing field
    }

Идентификатор field может быть использован только в методах доступа к свойству.

Backing field будет сгенерировано для свойства, если оно использует стандартную реализацию как минимум одного из методов доступа. Или в случае, когда пользовательский метод доступа ссылается на него через идентификатор field.

Например, в нижестоящем примере не будет никакого backing field:

val isEmpty: Boolean
    get() = this.size == 0
Backing Properties

Если вы хотите предпринять что-то такое, что выходит за рамки вышеуказанной схемы "неявного backing field", вы всегда можете использовать backing property:

private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
    get() {
        if (_table == null) {
            _table = HashMap() // параметры типа вычисляются автоматически (ориг.: "Type parameters are inferred")
        }
        return _table ?: throw AssertionError("Set to null by another thread")
    }

Такой подход ничем не отличается от подхода в Java, так как доступ к приватным свойствам со стандартными геттерами и сеттерами оптимизируется таким образом, что вызов функции не происходит.

Константы времени компиляции

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

  • Находиться на самом высоком уровне или быть членом объекта object

  • Быть проинициализированными значением типа String или значением примитивного типа

  • Не иметь переопределённого геттера

Такие свойства могут быть использованы в аннотациях:

const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }

Свойства с поздней инициализацией

Обычно, свойства, объявленные non-null типом, должны быть проинициализированы в конструкторе. Однако, довольно часто это неосуществимо. К примеру, свойства могут быть инициализированы через внедрение зависимостей, в установочном методе (ориг.: "setup method") юнит-теста или в методе onCreate в Android. В таком случае вы не можете обеспечить non-null инициализацию в конструкторе, но всё равно хотите избежать проверок на null при обращении внутри тела класса к такому свойству.

Для того, чтобы справиться с такой задачей, вы можете пометить свойство модификатором lateinit:

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method()  // объект инициализирован, проверять на null не нужно
    }
}

Такой модификатор может быть использован только с var свойствами, объявленными внутри тела класса (не в главном конструкторе, и только тогда, когда свойство не имеет пользовательских геттеров и сеттеров) и, начиная с Kotlin 1.2, со свойствами, расположенными на верхнем уровне, и локальными переменными. Тип такого свойства должен быть non-null и не должен быть примитивным.

Доступ к lateinit свойству до того, как оно проинициализировано, выбрасывает специальное исключение, которое чётко обозначает, что свойство не было определено.

Проверка инициализации lateinit var (начиная с версии 1.2)

Чтобы проверить, была ли проинициализировано lateinit var свойство, используйте .isInitialized метод ссылки на это свойство:

if (foo::bar.isInitialized) {
    println(foo.bar)
}

Эта проверка возможна только для лексически доступных свойств, то есть объявленных в том же типе, или в одном из внешних типов, или расположенных на верхнем того же файла.

Переопределение свойств

Делегированные свойства

Самый распространённый тип свойств просто считывает (или записывает) данные из backing field. Тем не менее, с пользовательскими геттерами и сеттерами мы можем реализовать совершенно любое поведение свойства. В реальности, существуют общепринятые шаблоны того, как могут работать свойства. Несколько примеров: * Вычисление значения свойства при первом доступе к нему (ленивые свойства) * Чтение из ассоциативного списка с помощью заданного ключа * Доступ к базе данных * Оповещение listener’а в момент доступа и т.п.

Такие распространённые поведения свойств могут быть реализованы в виде библиотек с помощью делегированных свойств.


Изолированные классы

Изолированные классы используются для отражения ограниченных иерархий классов, когда значение может иметь тип только из ограниченного набора, и никакой другой. Они являются, по сути, расширением enum-классов: набор значений enum типа также ограничен, но каждая enum-константа существует только в единственном экземпляре, в то время как наследник изолированного класса может иметь множество экземпляров, которые могут нести в себе какое-то состояние.

Чтобы описать изолированный класс, укажите модификатор sealed перед именем класса. Изолированный класс может иметь наследников, но все они должны быть объявлены в том же файле, что и сам изолированный класс. (До версии Kotlin 1.1 правила были ещё более строгими: классы должны были быть вложены в объявлении изолированного класса).

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

fun eval(expr: Expr): Double = when (expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
}

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

Ключевое преимущество от использования изолированных классов проявляется тогда, когда вы используете их в выражении when. Если возможно проверить что выражение покрывает все случаи, то вам не нужно добавлять else.

fun eval(expr: Expr): Double = when(expr) {
    is Expr.Const -> expr.number
    is Expr.Sum -> eval(expr.e1) + eval(expr.e2)
    Expr.NotANumber -> Double.NaN
    // оператор `else` не требуется, потому что мы покрыли все возможные случаи
}

Ключевое слово this

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

Если ключевое слово this не имеет определителей, то оно ссылается на область самого глубокого замыкания. Чтобы сослаться на this в одной из внешних областей, используются метки-определители:

this с определителем

Чтобы получить доступ к this из внешней области (класса, функции-расширения, или именованных литералов функций с принимающим объектом) мы пишем this@label, где @label - это метка области, из которой нужно получить this:

class A { // неявная метка @A
    inner class B { // неявная метка @B
        fun Int.foo() { // неявная метка @foo
            val a = this@A // this из A
            val b = this@B // this из B

            val c = this // принимающий объект функции foo(), типа Int
            val c1 = this@foo // принимающий объект функции foo(), типа Int

            val funLit = lambda@ fun String.() {
                val d = this // принимающий объект литерала funLit
            }

            val funLit2 = { s: String ->
                // принимающий объект функции foo(), т.к. замыкание лямбды не имеет принимающего объекта
                val d1 = this
            }
        }
    }
}

Модификаторы доступа

Классы, объекты, интерфейсы, конструкторы, функции, свойства и их сеттеры могут иметь модификаторы доступа (у геттеров всегда такая же видимость, как у свойств, к которым они относятся). В <b>Kotlin</b> предусмотрено четыре модификатора доступа: private, protected, internal и public. Если явно не используется никакого модификатора доступа, то по умолчанию применяется public.

Ниже вы найдёте описание всех возможных способов задавать область видимости.

Пакеты

Функции, свойства, классы, объекты и интерфейсы могут быть объявлены на самом "высоком уровне" прямо внутри пакета:

// имя файла: example.kt
package foo

fun baz() {}
class Bar {}
  • Если вы не укажете никакого модификатора доступа, будет использован public. Это значит, что весь код данного объявления будет виден из космоса;

  • Если вы пометите объявление словом private, оно будет иметь видимость только внутри файла, где было объявлено;

  • Если вы используете internal, видимость будет распространяться на весь модуль;

  • protected запрещено использовать в объявлениях "высокого уровня".

Примеры:

// file name: example.kt
package foo

private fun foo() {} // имеет видимость внутри example.kt

public var bar: Int = 5 // свойство видно со дна Марианской впадины
    private set         // сеттер видно только внутри example.kt

internal val baz = 6    // имеет видимость внутри модуля

Классы и интерфейсы

Для членов, объявленых в классе:

  • private означает видимость только внутри этого класса (включая его членов);

  • protected — то же самое, что и private + видимость в субклассах;

  • internal — любой клиент внутри модуля, который видит объявленный класс, видит и его internal члены;

  • public — любой клиент, который видит объявленный класс, видит его public члены.

Примечание для Java программистов: в <b>Kotlin</b> внешний класс не видит private члены своих вложенных классов.

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

Примеры:

open class Outer {
    private val a = 1
    protected open val b = 2
    internal val c = 3
    val d = 4  // public по умолчанию

    protected class Nested {
        public val e: Int = 5
    }
}

class Subclass : Outer() {
    // a не видно
    // b, c и d видно
    // класс Nested и e видно

    override val b = 5   // 'b' - protected
}

class Unrelated(o: Outer) {
    // o.a, o.b не видно
    // o.c и o.d видно (тот же модуль)
    // Outer.Nested не видно, и Nested::e также не видно
}

<a name="constructors"></a>

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

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

class C private constructor(a: Int) { ... }

В этом примере конструктор является private. По умолчанию все конструкторы имеют модификатор доступа public, то есть видны везде, где виден сам класс (а вот конструктор internal класса видно только в том же модуле).

Локальные объявления

Локальные переменные, функции и классы не могут иметь модификаторов доступа. <!--rcd27: неожиданно…​-→

<a name="modules"></a>

Модули

Модификатор доступа internal означает, что этот член видно в рамках его модуля. Модуль - это набор скомпилированных вместе <b>Kotlin</b> файлов:

  • модуль в IntelliJ IDEA;

  • Maven или Gradle проект;

  • набор скомпилированных вместе файлов с одним способом вызова <kotlinc> задачи в Ant.