Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user