Companion objects vs nested objects и зачем вообще нужны компаньоны

Очевидный ответ: для эмуляции 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 методами (из-за проблем с переопределением статических методов)

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *