Spec-Zone.ru › Kotlin 2

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

В Kotlin объекты позволяют определить класс и создать его экземпляр за один шаг. Это полезно, когда вам нужен повторно используемый экземпляр-одиночка или одноразовый объект. Для этих сценариев Kotlin предлагает два основных подхода: объявления объектов для создания одиночек и выражения объектов для создания анонимных одноразовых объектов.

Одиночка гарантирует, что у класса будет только один экземпляр, и предоставляет глобальную точку доступа к нему.

Объявления и выражения объектов лучше всего использовать в следующих случаях:

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

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

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

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

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

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

//sampleStart
// Declares a Singleton object to manage data providers
object DataProviderManager {
    private val providers = mutableListOf<DataProvider>()

    // Registers a new data provider
    fun registerDataProvider(provider: DataProvider) {
        providers.add(provider)
    }

    // Retrieves all registered data providers
    val allDataProviders: Collection<DataProvider> 
        get() = providers
}
//sampleEnd

// Example data provider interface
interface DataProvider {
    fun provideData(): String
}

// Example data provider implementation
class ExampleDataProvider : DataProvider {
    override fun provideData(): String {
        return "Example data"
    }
}

fun main() {
    // Creates an instance of ExampleDataProvider
    val exampleProvider = ExampleDataProvider()

    // To refer to the object, use its name directly
    DataProviderManager.registerDataProvider(exampleProvider)

    // Retrieves and prints all data providers
    println(DataProviderManager.allDataProviders.map { it.provideData() })
    // [Example data]
}

Объявление объекта инициализируется потокобезопасно при первом обращении к нему.

Чтобы обратиться к object, используйте его имя напрямую:

DataProviderManager.registerDataProvider(exampleProvider)

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

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

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

Как и объявления переменных, объявления объектов не являются выражениями, поэтому их нельзя использовать в правой части оператора присваивания:

// Syntax error: An object expression cannot bind a name.
val myObject = object MySingleton {
    val name = "Singleton"
}

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

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

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

object MyObject

fun main() {
    println(MyObject) 
    // MyObject@hashcode
}

Однако, пометив объявление объекта модификатором data, вы можете указать компилятору возвращать фактическое имя объекта при вызове toString(), как это происходит с классами данных:

data object MyDataObject {
    val number: Int = 3
}

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

Кроме того, компилятор генерирует несколько функций для data object:

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

  • equals()/hashCode() обеспечивают проверку на равенство и работу коллекций на основе хеширования

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

Функция equals() для data object гарантирует, что все объекты типа 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() function returns true:
    println(MySingleton == evilTwin) 
    // true

    // Don't compare data objects using ===
    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" (using 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() не генерируются.

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

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

В этом примере объявление EndOfFile как data object вместо обычного object означает, что для него будет доступна функция toString() без необходимости переопределять её вручную:

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

fun main() {
    println(Number(7)) 
    // Number(number=7)
    println(EndOfFile) 
    // EndOfFile
}

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

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

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

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

Вызывать члены companion object можно просто по имени класса:

class User(val name: String) {
    // Defines a companion object that acts as a factory for creating User instances
    companion object Factory {
        fun create(name: String): User = User(name)
    }
}

fun main(){
    // Calls the companion object's factory method using the class name as the qualifier. 
    // Creates a new User instance
    val userInstance = User.create("John Doe")
    println(userInstance.name)
    // John Doe
}

Имя companion object можно опустить — в этом случае используется имя Companion:

class User(val name: String) {
    // Defines a companion object without a name
    companion object { }
}

// Accesses the companion object
val companionUser = User.Companion

Члены класса могут обращаться к членам private соответствующего companion object:

class User(val name: String) {
    companion object {
        private val defaultGreeting = "Hello"
    }

    fun sayHi() {
        println(defaultGreeting)
    }
}
User("Nick").sayHi()
// Hello

Если использовать имя класса само по себе, оно будет ссылкой на объект-компаньон этого класса независимо от того, задано ли имя объекту-компаньону:

//sampleStart
class User1 {
    // Defines a named companion object
    companion object Named {
        fun show(): String = "User1's Named Companion Object"
    }
}

// References the companion object of User1 using the class name
val reference1 = User1

class User2 {
    // Defines an unnamed companion object
    companion object {
        fun show(): String = "User2's Companion Object"
    }
}

// References the companion object of User2 using the class name
val reference2 = User2
//sampleEnd

fun main() {
    // Calls the show() function from the companion object of User1
    println(reference1.show()) 
    // User1's Named Companion Object

    // Calls the show() function from the companion object of User2
    println(reference2.show()) 
    // User2's Companion Object
}

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

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

class User(val name: String) {
    // Defines a companion object that implements the Factory interface
    companion object : Factory<User> {
        override fun create(name: String): User = User(name)
    }
}

fun main() {
    // Uses the companion object as a Factory
    val userFactory: Factory<User> = User
    val newUser = userFactory.create("Example User")
    println(newUser.name)
    // Example User
}

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

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

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

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

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

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

fun main() {
//sampleStart
    val helloWorld = object {
        val hello = "Hello"
        val world = "World"
        // Object expressions extend the Any class, which already has a toString() function,
        // so it must be overridden
        override fun toString() = "$hello $world"
    }

    print(helloWorld)
    // Hello World
//sampleEnd
}

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

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

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

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

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

//sampleStart
// Creates an open class BankAccount with a balance property
open class BankAccount(initialBalance: Int) {
    open val balance: Int = initialBalance
}

// Defines an interface Transaction with an execute() function
interface Transaction {
    fun execute()
}

// A function to perform a special transaction on a BankAccount
fun specialTransaction(account: BankAccount) {
    // Creates an anonymous object that inherits from the BankAccount class and implements the Transaction interface
    // The balance of the provided account is passed to the BankAccount superclass constructor
    val temporaryAccount = object : BankAccount(account.balance), Transaction {

        override val balance = account.balance + 500  // Temporary bonus

        // Implements the execute() function from the Transaction interface
        override fun execute() {
            println("Executing special transaction. New balance is $balance.")
        }
    }
    // Executes the transaction
    temporaryAccount.execute()
}
//sampleEnd
fun main() {
    // Creates a BankAccount with an initial balance of 1000
    val myAccount = BankAccount(1000)
    // Performs a special transaction on the created account
    specialTransaction(myAccount)
    // Executing special transaction. New balance is 1500.
}

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

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

//sampleStart
class UserPreferences {
    private fun getPreferences() = object {
        val theme: String = "Dark"
        val fontSize: Int = 14
    }

    fun printPreferences() {
        val preferences = getPreferences()
        println("Theme: ${preferences.theme}, Font Size: ${preferences.fontSize}")
    }
}
//sampleEnd

fun main() {
    val userPreferences = UserPreferences()
    userPreferences.printPreferences()
    // Theme: Dark, Font Size: 14
}

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

Если функция или свойство, возвращающие анонимный объект, имеют видимость public, protected или internal, их фактический тип будет следующим:

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

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

  • Явно объявленный тип, если объявлено несколько супертипов.

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

//sampleStart
interface Notification {
    // Declares notifyUser() in the Notification interface
    fun notifyUser()
}

interface DetailedNotification

class NotificationManager {
    // The return type is Any. The message property is not accessible.
    // When the return type is Any, only members of the Any class are accessible.
    fun getNotification() = object {
        val message: String = "General notification"
    }

    // The return type is Notification because the anonymous object implements only one interface
    // The notifyUser() function is accessible because it is part of the Notification interface
    // The message property is not accessible because it is not declared in the Notification interface
    fun getEmailNotification() = object : Notification {
        override fun notifyUser() {
            println("Sending email notification")
        }
        val message: String = "You've got mail!"
    }

    // The return type is DetailedNotification. The notifyUser() function and the message property are not accessible
    // Only members declared in the DetailedNotification interface are accessible
    fun getDetailedNotification(): DetailedNotification = object : Notification, DetailedNotification {
        override fun notifyUser() {
            println("Sending detailed notification")
        }
        val message: String = "Detailed message content"
    }
}
//sampleEnd
fun main() {
    // This produces no output
    val notificationManager = NotificationManager()

    // The message property is not accessible here because the return type is Any
    // This produces no output
    val notification = notificationManager.getNotification()

    // The notifyUser() function is accessible
    // The message property is not accessible here because the return type is Notification
    val emailNotification = notificationManager.getEmailNotification()
    emailNotification.notifyUser()
    // Sending email notification

    // The notifyUser() function and message property are not accessible here because the return type is DetailedNotification
    // This produces no output
    val detailedNotification = notificationManager.getDetailedNotification()
}

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

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

import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent

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

    // MouseAdapter provides default implementations for mouse event functions
    // Simulates MouseAdapter handling mouse events
    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }

        override fun mouseEntered(e: MouseEvent) {
            enterCount++
        }
    })
    // The clickCount and enterCount variables are accessible within the object expression
}

Различия в поведении объявлений и выражений объектов

Поведение объявлений и выражений объектов при инициализации различается:

  • Выражения объектов выполняются (и инициализируются) немедленно в месте их использования.

  • Объявления объектов инициализируются лениво при первом обращении к ним.

  • Объект-компаньон инициализируется при загрузке (разрешении) соответствующего класса, что соответствует семантике статического инициализатора Java.

23 февраля 2025 г.
НаследованиеВыражения this

© 2010–2026 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