Files
obsidian/Kotlin/Синтаксис и типы/Базовые типы ООП/Object и companion object.md
T

348 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<T> {
fun parse(s: String): T?
}
class Color2(val rgb: Int) {
companion object : Parser<Color2> {
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> = 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)