Очевидный ответ: для эмуляции static методов, которые были в Java (и для interop’а используется аннотация @JvmStatic, генерирующая подобные джаве статик методы).
Но почему от static отказались в Kotlin и каково концептуальное значичение companion, ведь в Kotlin обычно всё делается не только для обеспечения interop с Java.
Самое интересное, что если объявить nested object — то это будет почти тоже самое… Дак всё же зачем?
Посмотрим на реализацию изнутри и сравним companion и nested objects:
class Example {
private val classMember = "class"
companion object {
private val companionMember = "companion object"
fun hello() = println("companion world")
}
object Nested {
private val nestedMember = "nested object"
fun hello() = println("nested world")
}
}
Что аналогично следующему Java коду:
public final class Example {
private final String classMember = "class";
private static final String companionMember = "companion object";
public static final Example.Companion companion = new Example.Companion();
public static final class Companion {
public final void hello() {
System.out.println("companion world");
}
}
public static final class Nested {
private static final String nestedMember = "nested object";
public static final Example.Nested INSTANCE = new Nested();
public final void hello() {
System.out.println("nested world");
}
}
}
Как видим, companion — суть «компаньон»: создаётся вместе с оболочкой, даже его поля хранятся в оболочке. Тогда как вложенный объект — именно котлиновский object class, с теми же свойствами: lazy initialization и прочее.
Погружение: смысл компаньонов.
Все упомянутые ниже пункты одинаково применимы как к вложенным объектам, так и к компаньонам, поэтому всё различие между ними в синтаксисе и в вышеупомянутой более тесной связи с оболочкой. (а вообще, ходят слухи, что компаньоны могут удалить в следующих версиях)
- Вместо статических методов в Котлин рекомендуется использовать функции расширения или функции верхнего уровня (уровня файла). Тогда namespace’ом является package.
- При том что функции расширения часто инлайнятся компилятором, повышая производительность
- В Java статик члены класса по сути своей не совсем «члены класса» — они работают совсем по-другому, чем обычные члены классов, например, не имеют доступа к нестатическим членам. По сути класс для статических членов является, своего рода, namespace’ом.
- Companion является таким же объектом — полностью «ООП’ным». Можно наследовать, реализовывать интерфейс. Можно передавать компаньон как аргумент функции.
- Кроме того, допустимы даже конструкции вида
open class Parent {
fun turnDown() = println("Turn down")
}
open class CompanionParent {
fun forWhat() = println("for what")
}
class Wrapper: Parent() {
companion object : CompanionParent()
fun what() {
turnDown()
forWhat()
}
}
Что? Множественное наследование? Д… неееее, но похоже 🙂
- Многие минусы статических членов класса не касаются компаньонов, например:
- Невозможность переопределение методов + путаница, вызываемая этим. Статические методы определяются ранним связыванием, то есть во время компиляции, тогда как обычные поздним — во время выполнения: объявив переменную родительского типа и вызвав статический метод, вызовется именно статический метод объявленного родительского типа — не важно, что в переменной хранится наследник с «переопределением»
- Статические поля не сериализуются + путаница, опять же. Компаньон же объект и есть объект.
- Сложности создания mock объектов при тестировании классов со static методами (из-за проблем с переопределением статических методов)