Spec-Zone.ru › Kotlin 1.9

Объектные выражения и объявления

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

Объектные выражения

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

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

Объектные выражения начинаются с ключевого слова object.

Если вам просто нужен объект, у которого нет каких-либо существенных супертипов, запишите его члены в фигурные скобки после object:

fun main() {
//sampleStart
    val helloWorld = object {
        val hello = "Hello"
        val world = "World"
        // object expressions extend Any, so `override` is required on `toString()`
        override fun toString() = "$hello $world"
    }
//sampleEnd
    print(helloWorld)
}

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

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

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

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

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

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

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

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

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

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

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

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

class C {
    // The return type is Any; x is not accessible
    fun getObject() = object {
        val x: String = "x"
    }

    // The return type is A; x is not accessible
    fun getObjectA() = object: A {
        override fun funFromA() {}
        val x: String = "x"
    }

    // The return type is B; funFromA() and x are not accessible
    fun getObjectB(): B = object: A, B { // explicit return type is required
        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++
        }
    })
    // ...
}

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

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

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

Объекты данных

При печати простого объявления object в Kotlin строковое представление содержит как его имя, так и хэш-код объекта:

object MyObject

fun main() {
    println(MyObject) // MyObject@1f32e575
}

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

  • toString() возвращает имя объекта данных

  • equals()/hashCode() пара

    Вы не можете предоставить собственную реализацию equals или hashCode для data object.

Функция toString() объекта данных возвращает имя объекта:

data object MyDataObject {
    val x: Int = 3
}

fun main() {
    println(MyDataObject) // MyDataObject
}

Функция equals() для data object гарантирует, что все объекты, имеющие тип вашего data object, считаются равными. В большинстве случаев у вас будет только один экземпляр вашего объекта данных во время выполнения (ведь data object объявляет синглтон). Однако в крайнем случае, когда другой объект того же типа генерируется во время выполнения (например, с помощью рефлексии платформы с java.lang.reflect или библиотеки сериализации JVM, которая использует этот API под капотом), это гарантирует, что объекты будут рассматриваться как равные.

Убедитесь, что вы сравниваете data objects структурно (используя оператор ==) и никогда по ссылке (используя оператор ===). Это помогает избежать проблем, когда существует более одного экземпляра объекта данных во время выполнения.

import java.lang.reflect.Constructor

data object MySingleton

fun main() {
    val evilTwin = createInstanceViaReflection()

    println(MySingleton) // MySingleton
    println(evilTwin) // MySingleton

    // Even when a library forcefully creates a second instance of MySingleton, its `equals` method returns true:
    println(MySingleton == evilTwin) // true

    // Do not compare data objects via ===.
    println(MySingleton === evilTwin) // false
}

fun createInstanceViaReflection(): MySingleton {
    // Kotlin reflection does not permit the instantiation of data objects.
    // This creates a new MySingleton instance "by force" (i.e. Java platform reflection)
    // Don't do this yourself!
    return (MySingleton.javaClass.declaredConstructors[0].apply { isAccessible = true } as Constructor<MySingleton>).newInstance()
}

Сгенерированная функция hashCode() имеет поведение, согласованное с функцией equals(), так что все экземпляры data object во время выполнения имеют одинаковый хэш-код.

Различия между объектами данных и классами данных

Хотя объявления data object и data class часто используются вместе и имеют некоторые сходства, существуют некоторые функции, которые не генерируются для data object:

  • Нет функции copy(). Поскольку объявление data object предназначено для использования в качестве объектов-синглтонов, функция copy() не генерируется. Паттерн синглтон ограничивает создание экземпляров класса одним экземпляром, что нарушилось бы, позволив создать копии экземпляра.

  • Нет функции componentN(). В отличие от data class, data object не имеет свойств данных. Поскольку попытка деструктурировать такой объект без свойств данных не имеет смысла, функции componentN() не генерируются.

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

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

sealed interface ReadResult
data class Number(val number: Int) : ReadResult
data class Text(val text: String) : ReadResult
data object EndOfFile : ReadResult

fun printReadResult(r: ReadResult) {
    when(r) {
        is Number -> println("Num(${r.number}")
        is Text -> println("Txt(${r.text}")
        is EndOfFile -> println("EOF")
    }
}

fun main() {
    printReadResult(EndOfFile) // EOF
}

Объекты-компаньоны

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

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

Члены объекта-компаньона могут вызываться просто с использованием имени класса в качестве квалификатора:

val instance = MyClass.create()

Имя объекта-компаньона может быть опущено, в этом случае будет использовано имя Companion:

class MyClass {
    companion object { }
}

val x = MyClass.Companion

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

Само имя класса (не как квалификатор другого имени) действует как ссылка на объект-компаньон класса (именованный или нет):

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.

Последнее изменение: 06 июля 2023
Встраиваемые классы значений Делегирование

© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/object-declarations.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API