Kotlin
April 2, 2024

Kotlin-Java interop: 'companion object' and 'private' visibility modifier

Заметка: данный пост представляет собой небольшое объяснение на частый вопрос "почему используешь private companion object с вложенными private const val?"

Оглавление


Дано

Рассмотрим пример с полями в companion object:

class KotlinClassSample {

    companion object {
        @JvmField
        val PUBLIC_STATIC_FIELD  = emptyList()
        val publicNonStaticField = emptyList()
        const val PUBLIC_STATIC_PRIMITIVE_FIELD = 0

        // JvmField has no effect on a private property
        private val privateField = emptyList()
        private const val PRIVATE_STATIC_PRIMITIVE_FIELD = 0
    }
}

Проверим доступ к KotlinClassSample коду из Java и Kotlin:

class JavaClassAccessTest {

    void callingKotlinFromJava() {
        // OK
        KotlinClassSample.PUBLIC_STATIC_FIELD;
        KotlinClassSample.PUBLIC_STATIC_PRIMITIVE_FIELD;
        KotlinClassSample.Companion.getPublicNonStaticField();
    }
}
class KotlinClassAccessTest {

    fun callingKotlinFromKotlin() {
        // OK
        KotlinClassSample.PUBLIC_STATIC_FIELD
        KotlinClassSample.PUBLIC_STATIC_PRIMITIVE_FIELD
        KotlinClassSample.publicNonStaticField
    }
}

Ожидаемое поведение на основе модификатора доступа public у companion object подтвердилось.

Теперь сменим модификатор на private и можем наблюдать следующее:

class JavaClassAccessTest {

    void callingKotlinFromJava() {
        // OK
        KotlinClassSample.PUBLIC_STATIC_FIELD;
        KotlinClassSample.PUBLIC_STATIC_PRIMITIVE_FIELD;

        // Error: 'Companion' has private access in 'KotlinClassSample'
        KotlinClassSample.Companion.getPublicNonStaticField(); // inaccessible
    }
}
class KotlinClassAccessTest {

    fun callingKotlinFromKotlin() {
        // Error: Cannot access 'Companion': it is private in 'KotlinClassSample'
        KotlinClassSample.PUBLIC_STATIC_FIELD           // inaccessible
        KotlinClassSample.PUBLIC_STATIC_PRIMITIVE_FIELD // inaccessible
        KotlinClassSample.publicNonStaticField          // inaccessible
    }
}

В голове у непосвященного человека может возникнуть резонный вопрос:

Почему так?⁠⁠

Решение

Чтобы понять "почему", декомпилируем KotlinClassSample c public companion object:

public final class KotlinClassSampleDecompiled {
    public static final CompanionObject Companion = new CompanionObject();

    public  static final int  PUBLIC_STATIC_PRIMITIVE_FIELD = 0;
    public  static final List PUBLIC_STATIC_FIELD  = EmptyList();
    private static final List publicNonStaticField = EmptyList();
    
    private static final int  PRIVATE_STATIC_PRIMITIVE_FIELD = 0;
    private static final List privateField = EmptyList();


    public static final class CompanionObject {

        public List getPublicNonStaticField() {
            return KotlinClassSampleDecompiled.publicNonStaticField;
        }
    }
}

И декомпилируем KotlinClassSample c private companion object:

public final class KotlinClassSampleDecompiled {
    private static final CompanionObject Companion = new CompanionObject();

    public  static final int  PUBLIC_STATIC_PRIMITIVE_FIELD = 0;
    public  static final List PUBLIC_STATIC_FIELD  = EmptyList();
    private static final List publicNonStaticField = EmptyList();

    private static final int  PRIVATE_STATIC_PRIMITIVE_FIELD = 0;
    private static final List privateField = EmptyList();


    private static final class CompanionObject {

        public List getPublicNonStaticField() {
            return KotlinClassSampleDecompiled.publicNonStaticField;
        }
    }
}
Заметка: содержимое Decompiled классов упрощено для повышения читабельности.

Как можно видеть, применение модификатора private к companion object изменило видимость только у класса CompanionObject, поля Companion, и как следствие геттера CompanionObject#getPublicNonStaticField.

Ответ

В общем-то магии никакой нет, и статические свойства объявленные в Kotlin через @JvmField или const val компилируются в Java как static final поля родительского класса, в который вкладывается сгенерированный класс CompanionObject.

Поэтому использование private companion object с вложенными private const val следует воспринимать нормально, возможно даже более "верно". Другое дело, если проект написан только на Kotlin, то private companion object будет достаточно для ограничения видимости.

Заметка: на последующих этапах компиляции применяются оптимизации и базовые типы объявленные через const будут подставленны в место их вызова, как шаблоны в C++, но это отдельная тема :)

Дополнительно прилагаю пример с методами, вывод аналогичен - статические функции\методы компилируются к родительскому классу, см. декомпилированный код внизу.

Дано:

class KotlinClassSample {

    companion object {
        @JvmStatic
        fun publicStaticFun() = Unit
        fun publicNonStaticFun() = Unit

        @JvmStatic
        private fun privateStaticFun() = Unit
        private fun privateNonStaticFun() = Unit
    }
}

Проверяем доступ к KotlinClassSample коду из Java и Kotlin:

class JavaClassAccessTest {

    void callingKotlinFromJava() {
        // OK
        KotlinClassSample.publicStaticFun();
        KotlinClassSample.Companion.publicStaticFun();
        KotlinClassSample.Companion.publicNonStaticFun();
    }
}
class KotlinClassAccessTest {

    fun callingKotlinFromKotlin() {
        // OK
        KotlinClassSample.publicStaticFun()
        KotlinClassSample.publicNonStaticFun()
    }
}

Изменим модификатор доступа у companion object на private и посмотрим, что изменилось:

class JavaClassAccessTest {

    void callingKotlinFromJava() {
        // OK
        KotlinClassSample.publicStaticFun();

        // Error: 'KotlinClassSample.Companion' has private access
        KotlinClassSample.Companion.publicStaticFun();    // inaccessible
        KotlinClassSample.Companion.publicNonStaticFun(); // inaccessible
    }
}
class KotlinClassAccessTest {

    fun callingKotlinFromKotlin() {
        // Error: Cannot access 'Companion': it is private in 'KotlinClassSample'
        KotlinClassSample.publicStaticFun()    // inaccessible
        KotlinClassSample.publicNonStaticFun() // inaccessible
    }
}

Декомпилированный KotlinClassSample c public companion object:

public final class KotlinClassSampleDecompiled {
 
    public static final CompanionObject Companion = new CompanionObject();

    public static void publicStaticFun() {
        Companion.publicStaticFun();
    }

    private static void privateStaticFun() {
        Companion.privateStaticFun();
    }

    public static final class CompanionObject {
  
        public void publicStaticFun() {}

        public void publicNonStaticFun() {}
        
        private void privateStaticFun() {}

        private void privateNonStaticFun() {}
    }
}

Декомпилированный KotlinClassSample c private companion object:

public final class KotlinClassSampleDecompiled {

    private static final CompanionObject Companion = new CompanionObject();

    public static void publicStaticFun() {
        Companion.publicStaticFun();
    }

    private static void privateStaticFun() {
        Companion.privateStaticFun();
    }

    private static final class CompanionObject {

        public void publicStaticFun() {}

        public void publicNonStaticFun() {}

        private void privateStaticFun() {}

        private void privateNonStaticFun() {}
    }
}

Литература