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

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

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

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

Создание анонимных объектов с нуля

Анонимный объект начинается с ключевого слова object.

Если вам просто нужен объект, у которого нет нетривиальных супертипов, перечислите его члены в фигурных скобках после object.

val helloWorld = object {
    val hello = "Hello"
    val world = "World"
    // тип анонимных объектов - Any, поэтому `override` необходим в `toString()`
    override fun toString() = "$hello $world"
}

Наследование анонимных объектов от супертипов

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

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
}

Использование анонимных объектов в качестве возвращаемых типов значений

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

class C {
    private fun getObject() = object {
        val x: String = "x"
    }

    fun printX() {
        println(getObject().x)
    }
}

Если эта функция или свойство маркированы как встроенные и public или private одновременно, то их тип:

  • Any, если анонимный объект не имеет объявленного супертипа;
  • Объявленный супертип анонимного объекта, если существует ровно один такой тип;
  • Явно объявленный тип, если существует более одного объявленного супертипа.

Во всех этих случаях члены, добавленные в анонимный объект, недоступны. Переопределенные члены доступны, если они объявлены в фактическом типе функции или свойства.

interface A {
    fun funFromA() {}
}
interface B

class C {
    // Возвращаемый тип Any. x недоступен
    fun getObject() = object {
        val x: String = "x"
    }

    // Возвращаемый тип A; x недоступен
    fun getObjectA() = object: A {
        override fun funFromA() {}
        val x: String = "x"
    }

    // Возвращаемый тип B; funFromA() и x недоступны
    fun getObjectB(): B = object: A, B { // требуется явный тип возвращаемого значения
        override fun funFromA() {}
        val x: String = "x"
    }
}

Доступ к переменным из анонимных объектов

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

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++
        }
    })
    // ...
}

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

Синглтон - очень полезный паттерн программирования, и Kotlin позволяет объявлять его довольно простым способом:

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) { /*...*/ }
}

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

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

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

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

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

val instance = MyClass.create()

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

class MyClass {
    companion object { }
}

val x = MyClass.Companion

Члены класса могут получить доступ к private членам соответствующего вспомогательного объекта.

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

class MyClass1 {
    companion object Named { }
}

val x = MyClass1

class MyClass2 {
    companion object { }
}

val y = MyClass2

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

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

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

val f: Factory<MyClass> = MyClass

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

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

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

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