13 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Вложенные и внутренние классы | Вложенный класс по умолчанию, inner для ссылки на внешний объект и утечки памяти |
|
Класс внутри класса по умолчанию не имеет ссылки на внешний объект. Чтобы её получить, нужен 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 Nestedclass Nested(по умолчанию)class Inner(без static)inner class Innerouter.new Inner()outer.Inner()Outer.this.fieldthis@Outer.fieldOuter.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 Далее: Полиморфизм