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

14 KiB
Raw Blame History

title, description, tags
title description tags
Object и companion object Синглтон через object, объект-выражение, companion object вместо static и фабрики
kotlin
oop

Слово object в Kotlin означает три разные вещи: объявление синглтона, выражение-анонимный объект и компаньон класса.

object - синглтон

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 через механизм инициализации классов.

object Lazy2 {
    init { println("инициализация") }
    val value = 42
}

fun main() {
    println("до обращения")        // до обращения
    println(Lazy2.value)           // инициализация, затем 42
    println(Lazy2.value)           // 42 — повторной инициализации нет
}

Объект может наследоваться и реализовывать интерфейсы:

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-иерархиях):

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.

Объект-выражение

Анонимный объект создаётся на месте:

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, выражение создаёт новый экземпляр при каждом вычислении:

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 — независимые объекты
}

Можно реализовать несколько интерфейсов сразу и даже ни одного:

fun main() {
    val point = object {
        val x = 1
        val y = 2
        override fun toString() = "($x, $y)"
    }
    println(point)       // (1, 2)
    println(point.x)     // 1
}

Тип такого объекта анонимен, поэтому за пределы локальной области он не выходит:

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

Объект-выражение видит и может менять локальные переменные:

fun main() {
    var clicks = 0
    val listener = object : Runnable {
        override fun run() { clicks++ }
    }
    listener.run(); listener.run()
    println(clicks)       // 2
}

companion object

Члены, привязанные к классу, а не к экземпляру:

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
}

Обращение идёт через имя класса - как к статическим членам. Приватный конструктор + фабрика в компаньоне - стандартный способ контролировать создание объектов.

Компаньон может быть только один, и у него может быть имя:

class User {
    companion object Factory {
        fun create() = User()
    }
}

fun main() {
    println(User.create()::class.simpleName)          // User
    println(User.Factory.create()::class.simpleName)  // User — через имя
}

Имя нужно редко - в основном для вызова из Java.

Компаньон - это объект

Поэтому он может реализовывать интерфейсы:

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 так передать нельзя - это реальное преимущество.

Расширения компаньона

class User(val name: String) {
    companion object
}

fun User.Companion.anonymous() = User("анонимный")

fun main() {
    println(User.anonymous().name)     // анонимный
}

Пустой companion object объявляют именно для этого - чтобы к классу можно было добавить «статическую» фабрику извне.

const и @JvmStatic

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

object или функции верхнего уровня

// вариант 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-класса с двойной проверкой блокировки:

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, Вложенные классы в Java

[!warning] Подводные камни object - глобальное изменяемое состояние, если в нём есть var:

object Counter {
    var count = 0        // общий на всю программу, не потокобезопасен
}

fun main() {
    Counter.count++
    println(Counter.count)    // 1
}

Инкремент из нескольких потоков потеряет обновления. Нужен AtomicInteger или синхронизация. Подробнее: Разделяемое состояние

Синглтон затрудняет тестирование. Состояние object переживает тесты, и порядок их выполнения начинает влиять на результат. Для подменяемых зависимостей лучше внедрение через конструктор.

Инициализация object ленивая - и порядок может удивить:

object A {
    val value = B.value + 1
}
object B {
    val value = 10
}

fun main() { println(A.value) }   // 11

При круговой зависимости между объектами одно из значений окажется null или 0, без всякой ошибки.

const val нельзя применить к сложному типу:

object Config {
    const val NAME = "app"           // ok
    // const val LIST = listOf(1)    // ошибка: только примитивы и String
    val LIST = listOf(1)             // так
}

fun main() { println("${Config.NAME} ${Config.LIST}") }

Компаньон не наследуется. У подкласса нет доступа к компаньону родителя как к своему:

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:

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 Далее: Typealias