Files
obsidian/Kotlin/ООП/Вложенные и внутренние классы.md
T

13 KiB

title, description, tags
title description tags
Вложенные и внутренние классы Вложенный класс по умолчанию, inner для ссылки на внешний объект и утечки памяти
kotlin
oop

Класс внутри класса по умолчанию не имеет ссылки на внешний объект. Чтобы её получить, нужен inner.

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

    class Nested2 {
        fun describe() = "вложенный, внешний объект недоступен"
        // fun bad() = secret        // ошибка: нет доступа
    }

    inner class Inner5 {
        fun describe() = "внутренний, вижу: $secret"
    }
}

fun main() {
    println(Outer5.Nested2().describe())        // внешний объект не нужен
    println(Outer5().Inner5().describe())       // нужен экземпляр Outer5
}
вложенный (по умолчанию) inner
ссылка на внешний объект нет есть
доступ к членам внешнего класса только к членам companion object ко всем, включая private
как создать Outer.Nested() outer.Inner()
аналог в Java static class обычный вложенный класс
риск утечки памяти нет есть

Обратите внимание на создание: Outer5().Inner5() - сначала внешний объект, затем внутренний на нём.

Зачем вложенные классы

Группировка по смыслу: класс нужен только в контексте внешнего и не имеет смысла отдельно.

class HttpClient2 {
    class Request2(val url: String, val method: String = "GET")
    class Response(val code: Int, val body: String)

    fun send(request: Request2): Response =
        Response(200, "ответ на ${request.method} ${request.url}")
}

fun main() {
    val client = HttpClient2()
    val response = client.send(HttpClient2.Request2("http://x.ru"))
    println("${response.code}: ${response.body}")   // 200: ответ на GET http://x.ru
}

Имя HttpClient2.Request2 само документирует принадлежность, и пакет не засоряется.

Частый случай - варианты sealed-иерархии:

sealed interface State3 {
    data object Loading3 : State3
    data class Ready(val data: String) : State3
    data class Error3(val message: String) : State3
}

fun render(s: State3) = when (s) {
    State3.Loading3 -> "загрузка"
    is State3.Ready -> "готово: ${s.data}"
    is State3.Error3 -> "ошибка: ${s.message}"
}

fun main() {
    println(render(State3.Ready("данные")))    // готово: данные
    println(render(State3.Loading3))            // загрузка
}

Подробнее: Sealed class и interface

Зачем inner

Когда внутренний объект логически часть конкретного экземпляра и должен видеть его состояние.

class Table(private val name: String) {
    private val rows = mutableListOf<String>()

    inner class Row(private val data: String) {
        fun insert(): String {
            rows.add(data)                     // меняем состояние внешнего
            return "в таблицу $name добавлено '$data', всего ${rows.size}"
        }
    }

    fun newRow(data: String) = Row(data)
    fun count() = rows.size
}

fun main() {
    val t = Table("users")
    println(t.newRow("Иван").insert())     // в таблицу users добавлено 'Иван', всего 1
    println(t.newRow("Пётр").insert())     // в таблицу users добавлено 'Пётр', всего 2
    println(t.count())                      // 2
}

this в inner-классе

При совпадении имён нужна квалификация метками:

class Outer6(val name: String = "внешний") {
    inner class Inner6(val name: String = "внутренний") {
        fun describe() = "${this.name} / ${this@Outer6.name}"
    }
}

fun main() {
    println(Outer6().Inner6().describe())      // внутренний / внешний
}

this@Outer6 - ссылка на внешний объект. Без метки this относится к ближайшему классу.

То же для super:

open class Base36 { open fun f() = "база" }

class Outer7 : Base36() {
    override fun f() = "внешний"

    inner class Inner7 {
        fun both() = "${f()} / ${super@Outer7.f()}"
    }
}

fun main() { println(Outer7().Inner7().both()) }   // внешний / база

Локальные классы

Класс можно объявить внутри функции:

fun process(items: List<Int>): String {
    val prefix = ">> "

    class Formatter(val value: Int) {
        fun format() = "$prefix$value"      // захватывает prefix
    }

    return items.map { Formatter(it).format() }.joinToString()
}

fun main() {
    println(process(listOf(1, 2)))     // >> 1, >> 2
}

Локальный класс видит переменные функции, включая изменяемые. Нужен редко: обычно хватает объекта-выражения или лямбды.

Вложенный интерфейс и object

class Service17 {
    fun interface Listener {              // интерфейс всегда "вложенный"
        fun onEvent(name: String): String
    }

    object Defaults {                      // object не может быть inner
        const val TIMEOUT = 30
    }

    companion object {
        fun create() = Service17()
    }

    fun notify2(l: Listener) = l.onEvent("старт")
}

fun main() {
    val s = Service17.create()
    println(s.notify2 { "получено: $it" })      // получено: старт
    println(Service17.Defaults.TIMEOUT)         // 30
}

Интерфейсы, object, enum и data class не могут быть inner - у них нет смысла быть привязанными к экземпляру.

Вложенность и дженерики

inner-класс видит параметры типа внешнего:

class Box3<T>(private val value: T) {
    inner class Wrapper {                    // T доступен
        fun get(): T = value
        fun describe() = "обёртка над ${value}"
    }

    class Helper4<U>(val other: U)           // вложенный: свой параметр типа
}

fun main() {
    val b = Box3("данные")
    println(b.Wrapper().get())               // данные
    println(b.Wrapper().describe())          // обёртка над данные
    println(Box3.Helper4(42).other)          // 42
}

Вложенный класс параметров внешнего не видит - ему нужны свои.

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

Java Kotlin
static class Nested class Nested (по умолчанию)
class Inner (без static) inner class Inner
outer.new Inner() outer.Inner()
Outer.this.field this@Outer.field
Outer.super.method() super@Outer.method()
анонимный класс объект-выражение

Значения по умолчанию перевёрнуты - и это важно. В Java вложенный класс по умолчанию держит ссылку на внешний объект, и чтобы этого избежать, нужно написать static. Разработчики об этом забывают, из-за чего возникают утечки памяти: внутренний объект, переживший внешний, не даёт собрать его сборщику мусора.

В Kotlin по умолчанию ссылки нет - её нужно запросить словом inner. Безопасный вариант стал умолчанием, а опасный требует явного решения. Это особенно заметно в Android, где утечки активити через нестатические внутренние классы и анонимные обработчики были массовой проблемой.

Outer.this против this@Outer. Синтаксис меток в Kotlin единообразен: та же @-нотация используется для return@forEach и this@apply в scope-функциях.

Сравнить: Вложенные классы в Java

[!warning] Подводные камни inner держит внешний объект в памяти - это утечка. Если внутренний объект переживает внешний (попал в долгоживущую коллекцию, кеш или обработчик), внешний не будет собран:

class Activity2(val heavyData: ByteArray = ByteArray(1024)) {
    inner class Handler3 {
        fun handle() = "обработка, внешний объект жив"
    }
}

val globalHandlers = mutableListOf<Activity2.Handler3>()

fun main() {
    globalHandlers.add(Activity2().Handler3())
    println(globalHandlers.first().handle())
    println("Activity2 не может быть собран: он держится ссылкой из Handler3")
}

Правило: inner только когда нужен доступ к состоянию внешнего объекта и время жизни внутреннего не превышает внешнего.

Вложенный класс не видит членов экземпляра внешнего класса - частая ошибка при переносе кода из Java, где поведение обратное:

class Outer8 {
    private val value = 1
    class Nested3 {
        // fun bad() = value          // ошибка
        fun ok() = "нужен inner или параметр"
    }
}

fun main() { println(Outer8.Nested3().ok()) }

Создание inner-класса требует экземпляра внешнего:

class Outer9 {
    inner class Inner8
    class Nested4
}

fun main() {
    println(Outer9.Nested4()::class.simpleName)      // Nested4
    println(Outer9().Inner8()::class.simpleName)     // Inner8
    // Outer9.Inner8()                               // ошибка
}

data class не может быть inner:

class Outer10 {
    data class Point5(val x: Int)        // вложенный — ок
    // inner data class Bad(val x: Int)  // ошибка
}

fun main() { println(Outer10.Point5(1)) }    // Point5(x=1)

Сгенерированные equals и copy не учитывали бы ссылку на внешний объект, что дало бы противоречивое поведение.

Глубокая вложенность ухудшает читаемость. Outer.Middle.Inner.Deep в сигнатурах - сигнал, что классы стоит вынести на верхний уровень или в отдельный файл.

Вложенный класс попадает в публичный API под составным именем. Переименование внешнего класса меняет полное имя вложенного - для библиотеки это ломающее изменение.

Локальный класс в функции захватывает переменные и продлевает их жизнь, как и лямбда. Если объект локального класса возвращается из функции, захваченные значения остаются в памяти.

Дополнительно: Object и companion object Далее: Полиморфизм