--- title: Object и companion object description: Синглтон через object, объект-выражение, companion object вместо static и фабрики tags: - kotlin - oop --- Слово `object` в Kotlin означает три разные вещи: объявление синглтона, выражение-анонимный объект и компаньон класса. ## object - синглтон ```kotlin object Config { val appName = "MyApp" var debug = false fun describe() = "$appName debug=$debug" } fun main() { println(Config.appName) // MyApp Config.debug = true println(Config.describe()) // MyApp debug=true } ``` Экземпляр создаётся **один** и **лениво** - при первом обращении. Потокобезопасность обеспечивает JVM через механизм инициализации классов. ```kotlin object Lazy2 { init { println("инициализация") } val value = 42 } fun main() { println("до обращения") // до обращения println(Lazy2.value) // инициализация, затем 42 println(Lazy2.value) // 42 — повторной инициализации нет } ``` Объект может наследоваться и реализовывать интерфейсы: ```kotlin interface Logger { fun log(msg: String) } object ConsoleLogger : Logger { override fun log(msg: String) = println("[LOG] $msg") } fun main() { val l: Logger = ConsoleLogger l.log("привет") // [LOG] привет } ``` Это главное отличие от набора статических методов: синглтон - полноценный объект, его можно передать туда, где ожидается интерфейс. У `object` не может быть конструктора - передать ему параметры при создании невозможно. ## data object Для случаев, где нужен осмысленный `toString` (обычно в [sealed-иерархиях](Sealed%20class%20и%20interface.md)): ```kotlin object Plain data object Loading fun main() { println(Plain) // Plain@1b6d3586 println(Loading) // Loading println(Loading == Loading) // true println(Loading.hashCode() == Loading.hashCode()) // true } ``` `data object` генерирует `toString`, `equals` и `hashCode`. Для синглтона `equals` и так работал по ссылке - полезен именно `toString`. ## Объект-выражение Анонимный объект создаётся на месте: ```kotlin interface ClickListener { fun onClick(x: Int, y: Int) } fun main() { val listener = object : ClickListener { override fun onClick(x: Int, y: Int) = println("клик $x,$y") } listener.onClick(1, 2) // клик 1,2 } ``` В отличие от объявления `object`, выражение создаёт **новый** экземпляр при каждом вычислении: ```kotlin private fun makeCounter() = object { var count = 0 fun inc() = ++count } fun main() { val a = makeCounter() val b = makeCounter() a.inc(); a.inc() println("${a.inc()} ${b.inc()}") // 3 1 — независимые объекты } ``` Можно реализовать несколько интерфейсов сразу и даже ни одного: ```kotlin fun main() { val point = object { val x = 1 val y = 2 override fun toString() = "($x, $y)" } println(point) // (1, 2) println(point.x) // 1 } ``` Тип такого объекта анонимен, поэтому за пределы локальной области он не выходит: ```kotlin class Holder { private fun make() = object { val x = 1 } // private — тип виден fun use() = make().x // fun bad() = object { val x = 1 } // public: тип станет Any, } // и x будет недоступен fun main() { println(Holder().use()) } // 1 ``` Объект-выражение видит и может менять локальные переменные: ```kotlin fun main() { var clicks = 0 val listener = object : Runnable { override fun run() { clicks++ } } listener.run(); listener.run() println(clicks) // 2 } ``` ## companion object Члены, привязанные к классу, а не к экземпляру: ```kotlin class User private constructor(val name: String) { companion object { const val MAX_NAME_LENGTH = 50 fun create(name: String): User? = if (name.length <= MAX_NAME_LENGTH) User(name) else null } } fun main() { println(User.MAX_NAME_LENGTH) // 50 println(User.create("Иван")?.name) // Иван println(User.create("x".repeat(99))) // null } ``` Обращение идёт через имя класса - как к статическим членам. Приватный конструктор + фабрика в компаньоне - стандартный способ контролировать создание объектов. Компаньон может быть только один, и у него может быть имя: ```kotlin class User { companion object Factory { fun create() = User() } } fun main() { println(User.create()::class.simpleName) // User println(User.Factory.create()::class.simpleName) // User — через имя } ``` Имя нужно редко - в основном для вызова из Java. ### Компаньон - это объект Поэтому он может реализовывать интерфейсы: ```kotlin interface Parser { fun parse(s: String): T? } class Color2(val rgb: Int) { companion object : Parser { override fun parse(s: String): Color2? = s.removePrefix("#").toIntOrNull(16)?.let { Color2(it) } } } fun main() { println(Color2.parse("#ff0000")?.rgb) // 16711680 val p: Parser = Color2 // компаньон как значение println(p.parse("#00ff00")?.rgb) // 65280 } ``` Статические методы в Java так передать нельзя - это реальное преимущество. ### Расширения компаньона ```kotlin class User(val name: String) { companion object } fun User.Companion.anonymous() = User("анонимный") fun main() { println(User.anonymous().name) // анонимный } ``` Пустой `companion object` объявляют именно для этого - чтобы к классу можно было добавить «статическую» фабрику извне. ## const и @JvmStatic ```kotlin class Config { companion object { const val VERSION = "1.0" // константа компиляции val runtime = System.nanoTime() // вычисляется при инициализации @JvmStatic fun helper() = "вызывается из Java как Config.helper()" } } fun main() { println(Config.VERSION) println(Config.helper()) } ``` `const` работает только для примитивов и `String`, и только на верхнем уровне, в `object` или в `companion object`. Значение подставляется в место использования при компиляции. `@JvmStatic` делает член настоящим статическим в байт-коде. Без неё Java-код обращается через `Config.Companion.helper()`. Подробнее: [Kotlin из Java](../../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) ## object или функции верхнего уровня ```kotlin // вариант 1: функции файла fun formatDate(d: Long) = "дата $d" // вариант 2: object object DateUtils { fun format(d: Long) = "дата $d" } ``` Если состояния нет и интерфейс реализовывать не нужно - функции верхнего уровня проще: не нужно писать префикс, и в байт-коде нет лишнего экземпляра. `object` оправдан, когда есть состояние или нужна реализация интерфейса. ## Три формы рядом | Форма | Экземпляров | Когда | |---|---|---| | `object Name { }` | один, ленивый | синглтон: конфиг, реестр, реализация интерфейса | | `object : Type { }` | новый каждый раз | колбэк, одноразовая реализация | | `companion object` | один на класс | фабрики, константы, «статика» | > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | `static` поля и методы | `companion object` или функции файла | > | `static final` константа | `const val` | > | паттерн Singleton (enum/holder) | `object` | > | анонимный класс `new Iface() {}` | `object : Iface {}` | > | `static` блок инициализации | `init` в `object`/`companion object` | > | статику нельзя передать как объект | компаньон реализует интерфейсы | > | лямбда только для SAM | `object` для любого числа методов | > > **Ключевое: в Kotlin нет `static`.** Это сделано намеренно - статика не участвует в полиморфизме и не реализует интерфейсы. Вместо неё либо функции верхнего уровня (для чистых утилит), либо `companion object` (когда член логически принадлежит классу). > > **Синглтон через `object` решает классическую задачу Java.** Корректный синглтон в Java требует либо `enum` с единственной константой, либо holder-класса с двойной проверкой блокировки: > ```java > class Config { > private Config() {} > private static class Holder { static final Config INSTANCE = new Config(); } > static Config getInstance() { return Holder.INSTANCE; } > } > ``` > В Kotlin это `object Config { }` - с той же ленивостью и потокобезопасностью. > > **Анонимный класс Java не может реализовать два интерфейса**, объект-выражение Kotlin - может. > > Сравнить: [Singleton в Java](../../../Программирование%20на%20Java/Паттерны%20проектирования/Singleton.md), [Вложенные классы в Java](../../../Программирование%20на%20Java/ООП/Вложенные%20классы.md) > [!warning] Подводные камни > **`object` - глобальное изменяемое состояние, если в нём есть `var`:** > ```kotlin > object Counter { > var count = 0 // общий на всю программу, не потокобезопасен > } > > fun main() { > Counter.count++ > println(Counter.count) // 1 > } > ``` > Инкремент из нескольких потоков потеряет обновления. Нужен `AtomicInteger` или синхронизация. Подробнее: [Разделяемое состояние](../../Корутины/Разделяемое%20состояние.md) > > **Синглтон затрудняет тестирование.** Состояние `object` переживает тесты, и порядок их выполнения начинает влиять на результат. Для подменяемых зависимостей лучше внедрение через конструктор. > > **Инициализация `object` ленивая - и порядок может удивить:** > ```kotlin > object A { > val value = B.value + 1 > } > object B { > val value = 10 > } > > fun main() { println(A.value) } // 11 > ``` > При круговой зависимости между объектами одно из значений окажется `null` или `0`, без всякой ошибки. > > **`const val` нельзя применить к сложному типу:** > ```kotlin > object Config { > const val NAME = "app" // ok > // const val LIST = listOf(1) // ошибка: только примитивы и String > val LIST = listOf(1) // так > } > > fun main() { println("${Config.NAME} ${Config.LIST}") } > ``` > > **Компаньон не наследуется.** У подкласса нет доступа к компаньону родителя как к своему: > ```kotlin > open class Base { > companion object { fun create() = "из Base" } > } > class Child : Base() > > fun main() { > println(Base.create()) // из Base > // println(Child.create()) // ошибка: unresolved reference > } > ``` > > **Тип объекта-выражения анонимен.** Если функция публичная, её тип результата схлопнется до супертипа (или `Any`), и дополнительные члены станут недоступны. > > **Из Java члены компаньона видны через `Companion`:** > ```kotlin > class Config { > companion object { > fun helper() = "без JvmStatic" > @JvmStatic fun helper2() = "с JvmStatic" > } > } > > fun main() { println("${Config.helper()} / ${Config.helper2()}") } > ``` > Java-код вызывает первую как `Config.Companion.helper()`, вторую - как `Config.helper2()`. > > **`object` внутри класса - не то же, что `companion object`.** Вложенный `object` требует обращения через своё имя: `Outer.Inner.member`. Дополнительно: [Sealed class и interface](Sealed%20class%20и%20interface.md) Далее: [Typealias](Typealias.md)