Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,347 @@
---
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)