14 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Object и companion object | Синглтон через object, объект-выражение, companion object вместо static и фабрики |
|
Слово 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