348 lines
14 KiB
Markdown
348 lines
14 KiB
Markdown
---
|
||
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)
|