Введение: «а где у вас static?»
«В Kotlin нет static — что вместо него?» — вопрос, который задают почти на каждом собеседовании. И ответ «companion object» — только половина правильного ответа. Вторая половина — что companion object статикой на самом деле не является, и от этого зависит куча вещей: от @JvmStatic до производительности.
До Java-переезда в Kotlin почти у всех болит голова от переноса статических методов. После переезда начинается второе разочарование: companion object — это не просто «static с другим синтаксисом», у него другая природа. Сегодня разберём, что это такое, как с ним работать и на чём сыпятся кандидаты. Всё по официальной документации Kotlin.
Что такое companion object на самом деле
Если коротко: companion object — это объект-одиночка, объявленный внутри класса. Ключевая фраза — «объект». В Kotlin есть object declarations: конструкция object Name { ... }, которая в одном месте объявляет класс и создаёт его единственный экземпляр — синглтон, причём с thread-safe инициализацией при первом обращении.
Companion object — тот же object, но вложенный в класс и помеченный ключевым словом companion:
class User(val name: String) {
companion object Factory {
fun create(name: String): User = User(name)
}
}
fun main() {
val user = User.create("Аня") // вызов через имя класса
}Зачем это нужно? Чтобы у класса были функции и свойства уровня класса, как в других языках — фабричные методы, константы, утилиты. В документации это описано так: companion object позволяет определять функции и свойства уровня класса.
И вот ключевой момент, который отличает сильного кандидата: члены companion object выглядят как static, но технически являются членами экземпляра companion object. У класса есть один экземпляр companion object, и его методы вызываются через него. Именно поэтому companion object может реализовывать интерфейсы — обычной статике такое недоступно:
interface Factory<T> {
fun create(name: String): T
}
class User(val name: String) {
companion object : Factory<User> {
override fun create(name: String): User = User(name)
}
}
fun main() {
val factory: Factory<User> = User // класс как экземпляр Factory
println(factory.create("Боря").name)
}Обратите внимание: val factory: Factory<User> = User — имя класса само является ссылкой на companion object. Это работает и для именованных, и для безымянных companion.
Главное
Companion object — это синглтон, вложенный в класс. Члены выглядят как static, но являются членами объекта. Поэтому companion может реализовывать интерфейсы, и поэтому для Java-кода его нужно дополнительно раскрывать через @JvmStatic.
Именованный и безымянный companion
Имя companion object можно задать, а можно опустить. Если имя не задано, объект называется Companion:
class Database {
companion object {
const val VERSION = 1
fun open(): Database = Database()
}
}
fun main() {
val db = Database.open()
println(Database.VERSION)
val companion = Database.Companion // явная ссылка на объект
}Что важно знать про доступы:
- Члены companion object вызываются через имя класса:
Database.open(). - Само имя класса — это ссылка на companion object:
val ref = Database. - Члены класса могут обращаться к private членам своего companion object напрямую, и наоборот — companion видит private члены класса.
- Companion object не может быть объявлен внутри функции (локальные object declarations запрещены), но может быть вложен в другой object.
Один нюанс, который любят спрашивать: const val работает только на верхнем уровне, в object или в companion object. Поэтому константы вроде VERSION — классический житель companion object. Требования те же, что и всегда: тип String или примитив, значение известно на этапе компиляции.
Companion object против Java static: @JvmStatic
Теперь самое интересное — Java-интероп. Если ваш Kotlin-класс вызывает код на Java (или его вызывают из Java — например, в проектах с легаси, или в тестах на Mockito), то Database.open() с точки зрения Java выглядит как Database.Companion.open(). Потому что на уровне байткода companion object — это обычный класс с полем Companion внутри Database.
Чтобы сгенерировать настоящие статические методы и поля, есть аннотация @JvmStatic для методов и @JvmField для свойств:
class LegacyBridge {
companion object {
@JvmStatic
fun open(): LegacyBridge = LegacyBridge()
@JvmField
val TAG = "LegacyBridge"
}
}
// Из Java: LegacyBridge.open() — настоящий static-метод
// и LegacyBridge.TAG — настоящее static-полеЭто подтверждает и документация Kotlin: на JVM члены companion object можно сгенерировать как настоящие статические методы и поля с помощью @JvmStatic.
Ошибка-классика на собеседовании: «companion object — это и есть static из Java, они одинаковы». Нет. Без @JvmStatic Java-код видит Database.Companion.open(), а не Database.open(). Статическим член делает только аннотация — и она нужна исключительно для Java-интеропа.
А ещё companion object — популярный способ сделать фабрику. Противники и сторонники спорят, но факт: фабричный метод в companion — стандартный паттерн, который встречается в официальных примерах. И отдельно — companion object часто путают с обычным object-синглтоном. Разница простая: object — самостоятельный синглтон (логирование, репозиторий-заглушка), а companion — привязан к своему классу и существует один на класс.
Где кандидаты ошибаются
Соберу промахи, которые реально слышу на собеседованиях и вижу в коде.
- «Companion object = static». Технически это экземпляр вложенного объекта. Static-члены на JVM создаёт только @JvmStatic / @JvmField.
- «Имя класса и Companion — разные вещи». Нет: имя класса — это и есть ссылка на companion object.
- «companion можно объявить внутри функции». Нельзя: object declarations не бывают локальными.
- «const val можно в обычном классе». Нельзя — только top-level, object или companion object.
- Путают object и companion. object — самостоятельный синглтон; companion — вложенный объект конкретного класса.
- Забывают про private-доступ. Класс и его companion видят private-члены друг друга — это частая тема вопросов.
Ещё одна ошибка — злоупотребление: класть в companion object всё подряд, «чтобы не создавать объект». Тяжёлые инициализации в companion выполняются при первом обращении к нему — и могут неожиданно подтормозить старт. Это не значит, что companion плохой — просто константы и фабрики туда, а тяжёлое состояние — в отдельный object или DI-контейнер.
Два уровня ответа
Первый уровень: «companion object заменяет static». Второй, сильный: «это вложенный синглтон, члены которого вызываются через имя класса; для Java-кода нужен @JvmStatic; класс и companion видят private друг друга». На собеседовании засчитывается второй.
Итоги
Companion object — механизм, который выглядит как замена static, но устроен иначе. Понимание этой разницы — то, что отличает кандидата с заученными ответами от человека, который понимает, как язык устроен.
- Природа: companion object — вложенный синглтон, члены которого вызываются через имя класса.
- Гибкость: может быть именованным или безымянным, реализовывать интерфейсы, видеть private-члены класса.
- Java-интероп: настоящую статику для Java дают
@JvmStaticи@JvmField.
Главное
Ответ на собеседовании: «companion object — это объект-одиночка внутри класса для функций и свойств уровня класса. В отличие от Java static, это полноценный объект: он может реализовывать интерфейсы. Для вызова из Java как static-членов нужны аннотации @JvmStatic и @JvmField».
Если чувствуете, что в Kotlin есть темы, где ответ обрывается на первом же уточняющем вопросе, — не нужно перебирать статьи наугад. Напишите в Telegram, разберём вашу ситуацию и составим план, что закрыть в первую очередь.