Объявления объектов и анонимные объекты
В Kotlin объекты позволяют определить класс и создать его экземпляр за один шаг. Это удобно, когда вам нужен либо многократно используемый экземпляр-синглтон, либо одноразовый объект. Для таких случаев Kotlin предоставляет два основных подхода: объявления объектов для создания синглтонов и анонимные объекты для создания одноразовых объектов без имени.
Синглтон гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа к нему.
Объявления объектов и анонимные объекты лучше всего подходят для следующих сценариев:
- Использование синглтонов для общих ресурсов. Нужно гарантировать, что во всём приложении существует только один экземпляр класса. Например, при управлении пулом подключений к базе данных.
- Создание фабричных методов. Нужен удобный способ эффективно создавать экземпляры. Вспомогательные объекты позволяют определять функции и свойства уровня класса, связанные с классом, что упрощает создание таких экземпляров и управление ими.
- Временное изменение поведения существующего класса. Нужно изменить поведение существующего класса без создания нового подкласса. Например, добавить объекту временную функциональность для конкретной операции.
- Необходимость типобезопасного дизайна. Нужны одноразовые реализации интерфейсов или абстрактных классов с помощью анонимных объектов. Это может пригодиться, например, для обработчика нажатия кнопки.
Объявления объектов (ориг.: Object declarations)
В Kotlin вы можете создавать единственные экземпляры объектов с помощью объявлений объектов, у которых после ключевого
слова object всегда указывается имя. Это позволяет определить класс и создать его экземпляр за один шаг, что удобно
для реализации синглтонов.
// Объявляет объект-синглтон для управления поставщиками данных
object DataProviderManager {
private val providers = mutableListOf<DataProvider>()
// Регистрирует нового поставщика данных
fun registerDataProvider(provider: DataProvider) {
providers.add(provider)
}
// Возвращает всех зарегистрированных поставщиков данных
val allDataProviders: Collection<DataProvider>
get() = providers
}
// Пример интерфейса поставщика данных
interface DataProvider {
fun provideData(): String
}
// Пример реализации поставщика данных
class ExampleDataProvider : DataProvider {
override fun provideData(): String {
return "Example data"
}
}
fun main() {
// Создаёт экземпляр ExampleDataProvider
val exampleProvider = ExampleDataProvider()
// Чтобы обратиться к объекту, используйте его имя напрямую
DataProviderManager.registerDataProvider(exampleProvider)
// Получает и печатает всех поставщиков данных
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) { ... }
}
Как и объявления переменных, объявления объектов не являются выражениями, поэтому их нельзя использовать в правой части оператора присваивания:
// Синтаксическая ошибка: анонимный объект не может связывать имя
val myObject = object MySingleton {
val name = "Singleton"
}
Объявления объектов не могут быть локальными, то есть их нельзя вкладывать непосредственно в функцию.
Однако они могут быть вложены в другие объявления объектов или в классы, которые не являются inner.
Объекты данных (data object)
Когда в 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 objectтолько структурно (с помощью оператора==) и никогда не сравнивайте их по ссылке (с помощью оператора===). Это помогает избежать ошибок, когда во время выполнения существует больше одного экземпляра объекта данных.
import java.lang.reflect.Constructor
data object MySingleton
fun main() {
val evilTwin = createInstanceViaReflection()
println(MySingleton)
// MySingleton
println(evilTwin)
// MySingleton
// Даже когда библиотека принудительно создаёт второй экземпляр MySingleton,
// его функция equals() возвращает true:
println(MySingleton == evilTwin)
// true
// Не сравнивайте объекты данных с помощью ===
println(MySingleton === evilTwin)
// false
}
fun createInstanceViaReflection(): MySingleton {
// Рефлексия Kotlin не разрешает создавать экземпляры объектов данных.
// Здесь новый экземпляр MySingleton создаётся "принудительно" с помощью платформенной рефлексии Java.
// Не делайте так в своём коде!
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 особенно полезны для изолированных иерархий, например
изолированных классов или интерфейсов. Они позволяют сохранить симметрию с классами данных,
которые вы могли определить рядом с объектом.
В этом примере объявление 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) {
// Определяет вспомогательный объект, который выступает фабрикой для создания экземпляров User
companion object Factory {
fun create(name: String): User = User(name)
}
}
fun main(){
// Вызывает фабричный метод вспомогательного объекта, используя имя класса в качестве определителя.
// Создаёт новый экземпляр User
val userInstance = User.create("John Doe")
println(userInstance.name)
// John Doe
}
Имя companion object можно опустить. В этом случае используется имя Companion:
class User(val name: String) {
// Определяет вспомогательный объект без имени
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
Когда имя класса используется само по себе, оно действует как ссылка на вспомогательный объект класса независимо от того, имеет этот объект имя или нет:
class User1 {
// Определяет именованный вспомогательный объект
companion object Named {
fun show(): String = "User1's Named Companion Object"
}
}
// Ссылается на вспомогательный объект User1 с помощью имени класса
val reference1 = User1
class User2 {
// Определяет вспомогательный объект без имени
companion object {
fun show(): String = "User2's Companion Object"
}
}
// Ссылается на вспомогательный объект User2 с помощью имени класса
val reference2 = User2
fun main() {
// Вызывает функцию show() из вспомогательного объекта User1
println(reference1.show())
// User1's Named Companion Object
// Вызывает функцию show() из вспомогательного объекта User2
println(reference2.show())
// User2's Companion Object
}
Хотя члены вспомогательных объектов в Kotlin выглядят как статические члены из других языков, на самом деле это члены экземпляра вспомогательного объекта, то есть они принадлежат самому объекту. Благодаря этому вспомогательные объекты могут реализовывать интерфейсы:
interface Factory<T> {
fun create(name: String): T
}
class User(val name: String) {
// Определяет вспомогательный объект, который реализует интерфейс Factory
companion object : Factory<User> {
override fun create(name: String): User = User(name)
}
}
fun main() {
// Использует вспомогательный объект как Factory
val userFactory: Factory<User> = User
val newUser = userFactory.create("Example User")
println(newUser.name)
// Example User
}
Однако на JVM вы можете сделать так, чтобы члены вспомогательных объектов генерировались как настоящие статические
методы и поля, если используете аннотацию @JvmStatic. Подробнее см. в разделе
Совместимость с Java.
Анонимные объекты (ориг.: Object expressions)
Анонимные объекты объявляют класс и создают экземпляр этого класса, не задавая имени ни классу, ни экземпляру. Такие классы полезны для одноразового использования. Их можно создавать с нуля, наследовать от существующих классов или использовать для реализации интерфейсов. Экземпляры таких классов также называются анонимными объектами, потому что они определяются выражением, а не именем.
Создание анонимных объектов с нуля
Анонимные объекты начинаются с ключевого слова object.
Если объект не расширяет классы и не реализует интерфейсы, его члены можно определить непосредственно внутри фигурных
скобок после ключевого слова object:
fun main() {
val helloWorld = object {
val hello = "Hello"
val world = "World"
// Анонимные объекты расширяют класс Any, в котором уже есть функция toString(),
// поэтому её нужно переопределить
override fun toString() = "$hello $world"
}
print(helloWorld)
// Hello World
}
Наследование анонимных объектов от супертипов
Чтобы создать анонимный объект, наследующийся от какого-либо типа или типов, укажите этот тип после object и
двоеточия (:). Затем реализуйте или переопределите члены этого класса так, как если бы вы
наследовали от него:
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { /*...*/ }
override fun mouseEntered(e: MouseEvent) { /*...*/ }
})
Если у супертипа есть конструктор, передайте в него соответствующие параметры. Несколько супертипов можно указать после двоеточия, разделяя их запятыми:
// Создаёт open-класс BankAccount со свойством balance
open class BankAccount(initialBalance: Int) {
open val balance: Int = initialBalance
}
// Определяет интерфейс Transaction с функцией execute()
interface Transaction {
fun execute()
}
// Функция для выполнения специальной транзакции с BankAccount
fun specialTransaction(account: BankAccount) {
// Создаёт анонимный объект, который наследуется от класса BankAccount и реализует интерфейс Transaction
// Баланс переданного счёта передаётся в конструктор суперкласса BankAccount
val temporaryAccount = object : BankAccount(account.balance), Transaction {
override val balance = account.balance + 500 // Временный бонус
// Реализует функцию execute() из интерфейса Transaction
override fun execute() {
println("Executing special transaction. New balance is $balance.")
}
}
// Выполняет транзакцию
temporaryAccount.execute()
}
fun main() {
// Создаёт BankAccount с начальным балансом 1000
val myAccount = BankAccount(1000)
// Выполняет специальную транзакцию для созданного счёта
specialTransaction(myAccount)
// Executing special transaction. New balance is 1500.
}
Использование анонимных объектов в качестве возвращаемых типов и типов значений
Когда вы возвращаете анонимный объект из локальной или private функции либо
свойства, все члены этого анонимного объекта доступны через эту функцию или свойство:
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}")
}
}
fun main() {
val userPreferences = UserPreferences()
userPreferences.printPreferences()
// Theme: Dark, Font Size: 14
}
Это позволяет возвращать анонимный объект с конкретными свойствами и даёт простой способ инкапсулировать данные или поведение без создания отдельного класса.
Если функция или свойство, возвращающие анонимный объект, имеют видимость public, protected или internal, их
фактический тип:
Any, если у анонимного объекта нет объявленного супертипа;- объявленный супертип анонимного объекта, если существует ровно один такой тип;
- явно объявленный тип, если объявленных супертипов больше одного.
Во всех этих случаях члены, добавленные в анонимный объект, недоступны. Переопределённые члены доступны, если они объявлены в фактическом типе функции или свойства. Например:
interface Notification {
// Объявляет notifyUser() в интерфейсе Notification
fun notifyUser()
}
interface DetailedNotification
class NotificationManager {
// Возвращаемый тип - Any. Свойство message недоступно.
// Когда возвращаемый тип - Any, доступны только члены класса Any.
fun getNotification() = object {
val message: String = "General notification"
}
// Возвращаемый тип - Notification, потому что анонимный объект реализует только один интерфейс.
// Функция notifyUser() доступна, потому что она является частью интерфейса Notification.
// Свойство message недоступно, потому что оно не объявлено в интерфейсе Notification.
fun getEmailNotification() = object : Notification {
override fun notifyUser() {
println("Sending email notification")
}
val message: String = "You've got mail!"
}
// Возвращаемый тип - DetailedNotification. Функция notifyUser() и свойство message недоступны.
// Доступны только члены, объявленные в интерфейсе DetailedNotification.
fun getDetailedNotification(): DetailedNotification = object : Notification, DetailedNotification {
override fun notifyUser() {
println("Sending detailed notification")
}
val message: String = "Detailed message content"
}
}
fun main() {
// Этот код ничего не выводит
val notificationManager = NotificationManager()
// Свойство message здесь недоступно, потому что возвращаемый тип - Any
// Этот код ничего не выводит
val notification = notificationManager.getNotification()
// Функция notifyUser() доступна.
// Свойство message здесь недоступно, потому что возвращаемый тип - Notification.
val emailNotification = notificationManager.getEmailNotification()
emailNotification.notifyUser()
// Sending email notification
// Функция notifyUser() и свойство message здесь недоступны, потому что возвращаемый тип - DetailedNotification.
// Этот код ничего не выводит
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 предоставляет реализации по умолчанию для функций событий мыши
// Имитирует обработку событий мыши с помощью MouseAdapter
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++
}
override fun mouseEntered(e: MouseEvent) {
enterCount++
}
})
// Переменные clickCount и enterCount доступны внутри анонимного объекта
}
Различия в поведении объявлений объектов и анонимных объектов
Между объявлениями объектов и анонимными объектами есть различия в поведении при инициализации:
- анонимные объекты выполняются и инициализируются сразу в месте использования;
- объявления объектов инициализируются лениво, при первом доступе;
- вспомогательный объект инициализируется, когда соответствующий класс загружается (разрешается), что соответствует семантике статического инициализатора Java.