Освой Kotlin играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Шаблон "Одиночка"
Объекты-компаньоны
Объекты-выражения
data object
Ключевое слово object встречается в разных случаях, но везде одна суть - определяет класс и одновременно создаёт экземпляр (объект) этого класса.
Можно реализовать шаблон "Одиночка". Например, класс для фонда заработной платы (у предприятий только один фонд), класс для печати (обработка заданий для принтера), Wi-Fi (общие настройки связи) и т.д.
Иногда требуется класс, который должен существовать в одном экземпляре. В Kotlin имеется специальный синтаксис объявления объекта для подобных случаев. Он как раз и сочетает в себе объявление класса и объявление одного экземпляра этого класса.
object One {
val cats = arrayListOf<Cat>()
fun callCat() {
for (cat in cats) {
...
}
}
}
Вы можете задавать свойства, методы, блоки инициализации, но не можете создавать конструкторы (как основные, так и вторичные). В отличие от экземпляров обычных классов, объявления объектов создаются непосредственно в точке определения, а не через вызов конструктора из других мест кода. Поэтому определять конструктор для объявления объекта не имеет смысла. Аналогичным образом, любое начальное состояние объявления объекта должно быть предоставлено как часть тела объекта.
Обращаться к методам и свойствам класса можно через имя объекта.
One.cats.add(Cat(...))
One.callCat()
Объекты также можно объявлять внутри существующего класса. Такие объекты существуют в единственном числе: у вас не будет отдельного объекта для каждого экземпляра класса-контейнера.
Этот подход часто используется для создания файлов-утилит. Например, создадим файл Utils.kt с кодом.
package ru.alexanderklimov.kotlin
object Utils {
val menuList = listOf(
"RED", "BLUE", "GREEN",
"YELLOW", "MAGENTA", "CYAN"
)
}
И теперь можете обращаться к переменной menuList из разных классов, например, из MainActivity через Utils.menuList. Студия может импортировать полное имя, поэтому можно обойтись и без приставки Utils.
textView.text = menuList[0]
Кроме того, объявления объектов могут наследоваться от классов и интерфейсов.
Но в некоторых случаях объект-одиночка может работать не так, как задумано. Например, воспользуемся GSON для сериализации и десериализации.
object MySingleton {
const val NAME: String = "MySingleton"
}
fun main() {
val gson = Gson()
// serialize
val json = gson.toJson(MySingleton)
// deserialize
val deserialized = gson.fromJson(json, MySingleton::class.java)
println("MySingleton before serialization hashCode: ${System.identityHashCode(MySingleton)}")
println("MySingleton after serialization hashCode: ${System.identityHashCode(deserialized)}")
println("Same instance: ${deserialized === MySingleton}")
}
MySingleton before serialization hashCode: 399534175
MySingleton after serialization hashCode: 428910174
Same instance: false
Мы получим разные объекты, хотя рассчитывали на один и тот же объект. GSON пришёл из мира Java и не знает о котлиновских объектах.
Если для вас это критично, то используйте другую Kotlin-библиотеку.
@Serializable
object MySingleton {
const val NAME: String = "MySingleton"
}
@OptIn(InternalSerializationApi::class)
fun main() {
val json = Json { encodeDefaults = true }
// serialize
val serialized = json.encodeToString(MySingleton)
// deserialize
val deserialized = json.decodeFromString(MySingleton::class.serializer(), serialized)
println("MySingleton before serialization hashCode: ${System.identityHashCode(MySingleton)}")
println("MySingleton after serialization hashCode: ${System.identityHashCode(deserialized)}")
println("Same instance: ${deserialized === MySingleton}")
}
MySingleton before serialization hashCode: 399534175
MySingleton after serialization hashCode: 399534175
Same instance: true
Также можно реализовать объект-компаньон, содержащий лишь фабричные методы, а также методы, связанные с классом, но не требующие обращения к его экземпляру. К членам такого объекта можно обращаться просто по имени класса.
В классах Kotlin не может быть статических членов, в нём нет ключевого слова static, как в Java. Вместо этого Kotlin полагается на функции уровня пакета (они способны заменить статические методы во многих ситуациях) и объявления объектов (они служат для замены статических методов и таких же полей в иных случаях). В большинстве случаев рекомендуется использовать функции верхнего уровня. Но они не могут получить доступ к приватным членам класса. Фабричный метод может служить примером функции, которой нужен доступ к приватным членам. Эти методы отвечают за создание объекта, и поэтому им часто требуется доступ к его приватным членам.
class MyClass {
companion object {
fun callMe() {
println("Companion object called")
}
}
}
fun main() {
MyClass.callMe()
// Companion object called
}
Важно помнить, что объект-компаньон принадлежит к соответствующему классу. Нельзя получить доступ к членам объекта-компаньона в экземпляре класса.
// Не работает
fun main() {
val myObject = MyClass()
myObject.callMe()
// Error: Unresolved reference: callMe
}
У объекта-компаньона есть доступ ко всем приватным членам класса, в том числе к приватному конструктору. Это делает его идеальным кандидатом для реализации паттерна «Фабрика».
Ещё можно использовать для записи объекта-выражения в качестве замены анонимного внутреннего класса.
Ключевое слово object можно использовать не только для объявления одиночек, но и для создания анонимных объектов, которые являются заменой анонимных внутренних классов в Java.
Можно создать типичный слушатель событий в Kotlin.
interface MouseListener {
fun onEnter()
fun onClick()
}
class Button(private val listener: MouseListener) { /* ... */ }
fun main() {
Button(object : MouseListener {
для реализации интерфейса MouseListener
override fun onEnter() { /* ... */ }
override fun onClick() { /* ... */ }
})
}
object : MouseListener - объявление анонимного объекта для реализации интерфейса MouseListener. Синтаксис такой же, как и при объявлении объектов, за исключением того, что имя объекта опущено. Но в отличие от объявлений объектов анонимные объекты не относятся к объектам-одиночкам. При каждом выполнении объектного выражения создаётся новый экземпляр объекта.
Объектное выражение объявляет класс и создаёт экземпляр этого класса, но не присваивает имя ни классу, ни экземпляру. Как правило, ни то ни другое не нужно, поскольку объект используется в качестве параметра в вызове функции. Если требуется присвоить объекту имя, то можно сохранить его в переменной:
val listener = object : MouseListener {
override fun onEnter() { /* ... */ }
override fun onClick() { /* ... */ }
}
В Kotlin 1.9 появились объекты данных. data object — это обычный object, но с реализацией по умолчанию функции toString(), которая выводит его имя без ручного её переопределения и с соответствием поведения определению data class. Соответствие поведения классам данных особенно актуально для иерархий запечатанных классов.
sealed interface ProfileScreenState {
data class Success(val username: String) : ProfileScreenState
data object Error : ProfileScreenState
data object Loading : ProfileScreenState
}
Объекты данных — это новый функционал Kotlin, который улучшает строковое представление object. Он особенно актуален, когда имеются иерархии запечатанных классов с другими классами данных и их нужно занести в журнал или распечатать для отладки или аналитики.