Files
obsidian/Kotlin/ООП/Модификаторы видимости.md
T

12 KiB
Raw Blame History

title, description, tags
title description tags
Модификаторы видимости public, internal, protected, private и чем internal отличается от package-private
kotlin
oop

Четыре модификатора. По умолчанию - public.

Модификатор Где виден
public везде (по умолчанию)
internal в пределах модуля
protected в классе и его подклассах
private в классе (или в файле, если член верхнего уровня)

На верхнем уровне

public val a = 1          // везде; слово public избыточно
internal val b = 2        // в своём модуле
private val c = 3         // только в этом файле

private fun helper() = c * 2

fun main() { println("$a $b ${helper()}") }    // 1 2 6

Для членов верхнего уровня private означает «виден в файле» - аналога «виден в пакете» у Kotlin нет. protected на верхнем уровне применить нельзя.

В классе

open class Service10 {
    val publicField = 1
    internal val internalField = 2
    protected val protectedField = 3
    private val privateField = 4

    fun show() = "$publicField $internalField $protectedField $privateField"
}

class Child2 : Service10() {
    fun access() = "$publicField $internalField $protectedField"
    // privateField недоступен
}

fun main() {
    val s = Service10()
    println(s.show())              // 1 2 3 4
    println(Child2().access())     // 1 2 3
    // println(s.protectedField)   // ошибка: protected
}

protected в Kotlin не даёт видимости в пакете - только в классе и подклассах. Это строже, чем в Java.

private в классе и вложенные классы

class Outer2 {
    private val secret = "скрыто"

    class Nested {
        // fun bad() = secret        // ошибка: нет доступа к членам Outer
        fun ok() = "вложенный класс"
    }

    inner class Inner2 {
        fun read() = secret          // inner видит внешний класс
    }
}

fun main() {
    println(Outer2.Nested().ok())        // вложенный класс
    println(Outer2().Inner2().read())    // скрыто
}

Подробнее: Вложенные и внутренние классы

internal - видимость в модуле

Модуль - это единица компиляции: модуль Gradle или Maven, набор файлов одной задачи kotlinc.

internal class Repository {
    internal fun load() = "данные"
}

internal fun helper2() = Repository().load()

fun main() { println(helper2()) }    // данные

internal - основной инструмент для библиотек: публичный API помечается public, всё остальное - internal, и клиент библиотеки не видит внутренностей.

Типичный приём - публичный интерфейс и внутренняя реализация:

interface UserRepository {
    fun find(id: Int): String?
}

internal class UserRepositoryImpl : UserRepository {
    override fun find(id: Int) = if (id == 1) "Иван" else null
}

fun createRepository(): UserRepository = UserRepositoryImpl()

fun main() {
    println(createRepository().find(1))      // Иван
}

Снаружи модуля видны только UserRepository и createRepository - реализацию заменить можно без ломающих изменений.

Видимость конструктора

class Service11 private constructor(val id: Int) {
    companion object {
        fun create(id: Int) = if (id > 0) Service11(id) else null
    }
}

fun main() {
    println(Service11.create(5)?.id)    // 5
    println(Service11.create(0))        // null
}

Слово constructor обязательно, когда у конструктора есть модификатор. Подробнее: Классы и конструкторы

Видимость сеттера

Геттер всегда имеет видимость самого свойства, а сеттер можно ограничить:

class Session2 {
    var token: String = ""
        private set

    var attempts: Int = 0
        internal set

    fun login(t: String) { token = t; attempts++ }
}

fun main() {
    val s = Session2()
    s.login("abc")
    println("${s.token} ${s.attempts}")    // abc 1
    // s.token = "x"                       // ошибка
}

Правила согласованности

Публичный член не может раскрывать тип с меньшей видимостью:

internal class Internal2

class Public2 {
    // fun bad(): Internal2 = Internal2()      // ошибка: public раскрывает internal
    internal fun ok(): Internal2 = Internal2()
    private fun alsoOk(): Internal2 = Internal2()
}

fun main() { println(Public2().ok()::class.simpleName) }   // Internal2

Компилятор проверяет это и для параметров, и для типов результата, и для супертипов.

Видимость и переопределение

Переопределённый член не может сужать видимость:

open class Base10 {
    open internal fun f() = "base"
    protected open fun g() = "g"
}

class Derived10 : Base10() {
    override fun f() = "derived"              // internal сохранена
    public override fun g() = "расширена"     // protected → public можно
}

fun main() {
    println(Derived10().f())      // derived
    println(Derived10().g())      // расширена — теперь доступен снаружи
}

Расширять видимость разрешено, сужать - нет: это нарушило бы контракт базового типа.

protected-член можно сделать public в подклассе - приём для постепенного раскрытия API.

Что видно из Java

Это важное практическое различие:

Kotlin В байт-коде Доступно из Java
public public да
internal public + искажённое имя да
protected protected да, в подклассе и в пакете
private (член класса) private нет
private (верхний уровень) private в файловом классе нет
class Service12 {
    internal fun internalMethod() = "в байт-коде это public"
    private fun privateMethod() = "действительно private"

    fun show() = "${internalMethod()} / ${privateMethod()}"
}

fun main() { println(Service12().show()) }

internal существует только на уровне компилятора Kotlin. В байт-коде член становится public, а к его имени добавляется суффикс с именем модуля - чтобы случайный вызов из Java выглядел странно, но он возможен. Полагаться на internal как на защиту от Java-кода нельзя.

Подробнее: Kotlin из Java

[!info] Отличия от Java

Java Kotlin
public public
нет модификатора = package-private нет аналога
protected = подклассы + пакет protected = только подклассы
private private
нет аналога internal = модуль
модули JPMS (Java 9+) internal работает всегда

По умолчанию public, а не package-private. Это ловушка при переходе: Java-разработчик не пишет модификатор, ожидая видимость в пакете, и случайно публикует API.

Package-private в Kotlin нет совсем. Замена - internal (шире: весь модуль) или private на верхнем уровне (уже: один файл). Пакет в Kotlin не является границей инкапсуляции, потому что пакеты легко расширить извне - любой может объявить класс в вашем пакете.

protected строже. В Java protected-член виден всему пакету, что часто неожиданно. В Kotlin - только подклассам.

internal решает задачу, для которой в Java нужны JPMS. Модульная система Java появилась в 9 и применяется редко; internal работает в любом проекте с модулями Gradle и не требует настройки.

Сравнить: Модификаторы в Java, Инкапсуляция

[!warning] Подводные камни Забыли модификатор - получили публичный API. Всё, что не помечено, видно всем и становится обязательством на поддержку.

internal не защищает от Java-кода - в байт-коде это public. Единственная настоящая защита - private или отдельный модуль.

private на верхнем уровне - это «виден в файле», а не «в пакете»:

private val fileLocal = "виден только здесь"
private class Helper3 { fun work() = fileLocal }

fun main() { println(Helper3().work()) }

Другой файл того же пакета доступа не получит.

private член класса виден во всём классе, включая вложенные объявления:

class Outer3 {
    private val secret = "скрыто"

    inner class Inner3 {
        fun read() = secret         // доступ есть
    }

    companion object {
        fun describe() = "компаньон не видит secret экземпляра"
    }
}

fun main() {
    println(Outer3().Inner3().read())    // скрыто
    println(Outer3.describe())
}

protected недоступен в расширениях - даже внутри того же класса:

open class Base11 {
    protected val value = 42
    fun show() = value
}

// fun Base11.bad() = value     // ошибка: protected недоступен в расширении

fun main() { println(Base11().show()) }    // 42

private конструктор не защищает data class от copy в версиях до Kotlin 2.1: клиент мог обойти фабрику через copy(). Подробнее: Data class

Расширение видимости при переопределении легко сделать случайно. public override fun у protected-метода публикует то, что автор базового класса прятал.

internal-классы в публичных сигнатурах - ошибка компиляции, но internal-тип внутри дженерика может просочиться:

internal class Impl2

class Api2 {
    internal val items = mutableListOf<Impl2>()
    val count: Int get() = items.size        // публично только число
}

fun main() { println(Api2().count) }    // 0

Дополнительно: ООП Далее: Наследование