Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,505 @@
---
title: Рефлексия
description: KClass, KProperty, KFunction, kotlin-reflect, ссылки на члены, интероп с Java Reflection
tags:
- kotlin
- reflection
---
Рефлексия - доступ к структуре программы во время выполнения: какие у класса свойства, какие параметры у функции, как вызвать то, что не известно на этапе компиляции. Kotlin даёт собственный API (`kotlin.reflect`) поверх Java Reflection, потому что в байт-коде не видно ни свойств, ни `suspend`, ни значений по умолчанию.
```kotlin
import kotlin.reflect.full.memberProperties
data class User(val id: Int, var name: String)
fun main() {
val user = User(1, "Иван")
User::class.memberProperties
.sortedBy { it.name }
.forEach { println("${it.name} = ${it.call(user)}") }
}
```
```
id = 1
name = Иван
```
## Два уровня API
Kotlin-рефлексия разделена на две части, и это важно практически.
```kotlin
// Доступно всегда, только kotlin-stdlib
import kotlin.reflect.KClass
enum class Status { OK }
data class P(val x: Int)
fun main() {
println(Status::class.simpleName)
println(P::class.qualifiedName)
println(P::class.java.name)
println(P::class.isInstance(P(1)))
}
```
```
Status
P
P
true
```
Без `kotlin-reflect` в classpath работают только `simpleName`, `qualifiedName`, `isInstance`, `java` и несколько похожих свойств. Всё остальное - `memberProperties`, `primaryConstructor`, `sealedSubclasses` - бросает `KotlinReflectionNotSupportedError`:
```
Exception in thread "main" kotlin.jvm.KotlinReflectionNotSupportedError:
Kotlin reflection implementation is not found at runtime.
Make sure you have kotlin-reflect.jar in the classpath
```
Подключается отдельно:
```kotlin
// build.gradle.kts
dependencies {
implementation(kotlin("reflect"))
}
```
`kotlin-reflect` весит около 3 МБ и тянет собственный парсер метаданных - для Android и serverless это значимо. Подробнее: [Gradle и Kotlin DSL](../Инструментарий/Gradle%20и%20Kotlin%20DSL.md)
## KClass
```kotlin
import kotlin.reflect.full.companionObject
import kotlin.reflect.full.declaredMemberFunctions
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
data class User(val id: Int, var name: String) {
val display: String get() = "#$id $name"
fun greet(greeting: String): String = "$greeting, $name"
companion object { const val TABLE = "users" }
}
fun main() {
val kc = User::class
println("simpleName=${kc.simpleName}")
println("isData=${kc.isData}")
println("isFinal=${kc.isFinal}")
println("visibility=${kc.visibility}")
println("primary params=${kc.primaryConstructor!!.parameters.map { it.name }}")
println("memberProperties=${kc.memberProperties.map { it.name }.sorted()}")
println("функции=${kc.declaredMemberFunctions.map { it.name }.sorted()}")
println("companionObject=${kc.companionObject?.simpleName}")
}
```
```
simpleName=User
isData=true
isFinal=true
visibility=PUBLIC
primary params=[id, name]
memberProperties=[display, id, name]
функции=[component1, component2, copy, equals, greet, hashCode, toString]
companionObject=Companion
```
Видно, что `data class` генерирует `componentN`, `copy`, `equals`, `hashCode`, `toString` - рефлексия показывает их наравне с написанными руками. Подробнее: [Data class](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Data%20class.md)
| Свойство `KClass` | Что даёт |
|---|---|
| `simpleName`, `qualifiedName` | имена; `null` для локальных и анонимных |
| `isData`, `isSealed`, `isInner`, `isCompanion`, `isValue`, `isFun` | характер класса |
| `isAbstract`, `isFinal`, `isOpen` | модальность |
| `visibility` | `PUBLIC`, `INTERNAL`, `PROTECTED`, `PRIVATE` |
| `constructors`, `primaryConstructor` | конструкторы |
| `memberProperties`, `declaredMemberProperties` | свойства с унаследованными и без |
| `memberFunctions`, `declaredMemberFunctions` | функции |
| `supertypes`, `superclasses` | иерархия |
| `sealedSubclasses` | подклассы `sealed`-типа |
| `nestedClasses` | вложенные классы |
| `objectInstance` | экземпляр `object`, иначе `null` |
| `companionObject`, `companionObjectInstance` | компаньон |
| `typeParameters` | параметры типа |
| `java` | мост к `Class<T>` |
### sealed, enum, object
```kotlin
import kotlin.reflect.full.isSubclassOf
sealed interface Shape {
data class Circle(val r: Double) : Shape
data class Square(val a: Double) : Shape
}
enum class Status { OK, FAIL }
object Single { val x = 1 }
open class Base
class Derived : Base()
fun main() {
println("sealedSubclasses=${Shape::class.sealedSubclasses.map { it.simpleName }}")
println("enum values=${Status::class.java.enumConstants.toList()}")
println("object instance=${Single::class.objectInstance?.x}")
println("supertypes=${Derived::class.supertypes}")
println("isSubclassOf=${Derived::class.isSubclassOf(Base::class)}")
}
```
```
sealedSubclasses=[Circle, Square]
enum values=[OK, FAIL]
object instance=1
supertypes=[Base]
isSubclassOf=true
```
`sealedSubclasses` даёт полный список вариантов - это основа для сериализаторов и фабрик по типу. Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
## Ссылки на члены
```kotlin
import kotlin.reflect.KFunction2
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.declaredMemberFunctions
data class User(val id: Int, var name: String) {
fun greet(greeting: String): String = "$greeting, $name"
}
fun topLevel(a: Int, b: String = "по умолчанию"): String = "$a-$b"
fun main() {
val user = User(1, "Иван")
// свойство как объект
@Suppress("UNCHECKED_CAST")
val nameProp = User::class.memberProperties
.first { it.name == "name" } as KMutableProperty1<User, String>
println("get=${nameProp.get(user)}")
nameProp.set(user, "Пётр")
println("после set=${user.name}")
// функция класса
val greet = User::class.declaredMemberFunctions.first { it.name == "greet" }
println("call=${greet.call(user, "Привет")}")
// ссылка на функцию верхнего уровня
val ref: KFunction2<Int, String, String> = ::topLevel
println("name=${ref.name}, параметров=${ref.parameters.size}")
println("call=${ref.call(1, "b")}")
println("callBy с дефолтом=${ref.callBy(mapOf(ref.parameters[0] to 5))}")
}
```
```
get=Иван
после set=Пётр
call=Привет, Пётр
name=topLevel, параметров=2
call=1-b
callBy с дефолтом=5-по умолчанию
```
| Тип | Что представляет |
|---|---|
| `KProperty0<V>` | свойство без получателя: `::topLevelVal`, `obj::prop` |
| `KProperty1<T, V>` | свойство класса: `User::name` |
| `KProperty2<T, R, V>` | свойство-расширение с двумя получателями |
| `KMutableProperty1<T, V>` | то же для `var` - добавляет `set` |
| `KFunction<R>` | функция; `KFunction1`, `KFunction2`, ... по арности |
| `KParameter` | параметр: `name`, `type`, `isOptional`, `kind` |
| `KType` | тип с аргументами и `isMarkedNullable` |
`call` и `callBy` различаются принципиально: `call` требует все аргументы по порядку, `callBy` принимает `Map<KParameter, Any?>` и **учитывает значения по умолчанию** для отсутствующих. Значения по умолчанию в байт-коде не видны, поэтому через Java Reflection `callBy` не воспроизвести. Подробнее: [Функции](../Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md)
### Приватные члены
```kotlin
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.jvm.isAccessible
class Hidden {
private val secret = 42
}
fun main() {
val prop = Hidden::class.declaredMemberProperties.first { it.name == "secret" }
val error = runCatching { prop.call(Hidden()) }.exceptionOrNull()
println("без isAccessible: ${error!!::class.simpleName}")
prop.isAccessible = true
println("с isAccessible: ${prop.call(Hidden())}")
}
```
```
без isAccessible: IllegalCallableAccessException
с isAccessible: 42
```
`isAccessible = true` - это Kotlin-обёртка над `setAccessible` из Java. На JDK 17+ она может упираться в строгую инкапсуляцию модулей для классов стандартной библиотеки; для собственного кода работает всегда.
## Типы
```kotlin
import kotlin.reflect.full.memberProperties
import kotlin.reflect.typeOf
class G {
val field: List<Map<String, Int>> = emptyList()
}
fun nullableParam(x: String?) = x
fun main() {
val t = typeOf<List<Map<String, Int>>>()
println("typeOf=$t")
println("classifier=${t.classifier}")
println("аргументы=${t.arguments}")
val prop = G::class.memberProperties.first()
println("тип свойства=${prop.returnType}")
println("вложенный аргумент=${prop.returnType.arguments[0].type}")
val p = ::nullableParam.parameters[0]
println("nullable=${p.type.isMarkedNullable}")
}
```
```
typeOf=kotlin.collections.List<kotlin.collections.Map<kotlin.String, kotlin.Int>>
classifier=class kotlin.collections.List
аргументы=[kotlin.collections.Map<kotlin.String, kotlin.Int>]
тип свойства=kotlin.collections.List<kotlin.collections.Map<kotlin.String, kotlin.Int>>
вложенный аргумент=kotlin.collections.Map<kotlin.String, kotlin.Int>
nullable=true
```
`KType` сохраняет аргументы типов и признак nullable - эта информация берётся из метаданных Kotlin, а не из байт-кода. Но это **статическая** информация: тип объявления, а не фактический тип значения.
```kotlin
fun main() {
val list: List<String> = listOf("a")
println("класс=${list::class.simpleName}")
println("параметры типа=${list::class.typeParameters.map { it.name }}")
}
```
```
класс=SingletonList
параметры типа=[E]
```
У значения узнать аргумент типа нельзя - дженерики стираются. `list::class` даёт лишь имя параметра `E`, а не `String`. Для сохранения типа нужен `reified` на этапе компиляции. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md)
## Делегаты
```kotlin
import kotlin.reflect.KProperty0
import kotlin.reflect.jvm.isAccessible
class Lazily {
val value: String by lazy { "вычислено" }
}
fun main() {
val obj = Lazily()
val prop: KProperty0<String> = obj::value
prop.isAccessible = true
println("делегат до чтения=${prop.getDelegate()}")
println("значение=${obj.value}")
println("делегат после=${prop.getDelegate()}")
}
```
```
делегат до чтения=Lazy value not initialized yet.
значение=вычислено
делегат после=вычислено
```
`getDelegate()` возвращает сам объект-делегат, а не значение свойства - для `by lazy` это `Lazy<T>`, чей `toString` до инициализации сообщает «not initialized yet». Это единственный способ узнать состояние ленивого свойства, не вычисляя его. Подробнее: [Делегирование](../ООП/Делегирование.md)
## Интероп с Java Reflection
```kotlin
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.javaConstructor
import kotlin.reflect.jvm.javaField
import kotlin.reflect.jvm.javaMethod
import kotlin.reflect.jvm.kotlinFunction
import kotlin.reflect.full.memberProperties
data class Point(val x: Int, val y: Int)
class Target {
lateinit var late: String
val lazyProp: Int by lazy { 7 }
}
fun topLevel(a: Int): Int = a
fun main() {
println("KClass -> Class=${Point::class.java.name}")
println("Class -> KClass=${Point::class.java.kotlin.simpleName}")
println("javaMethod=${::topLevel.javaMethod?.name}")
println("Method -> KFunction=${::topLevel.javaMethod?.kotlinFunction?.name}")
println("javaConstructor=${Point::class.primaryConstructor?.javaConstructor}")
val late = Target::class.memberProperties.first { it.name == "late" }
println("lateinit isLateinit=${late.isLateinit}, javaField=${late.javaField?.name}")
val lz = Target::class.memberProperties.first { it.name == "lazyProp" }
println("lazy javaField=${lz.javaField?.name}")
}
```
```
KClass -> Class=Point
Class -> KClass=Point
javaMethod=topLevel
Method -> KFunction=topLevel
javaConstructor=public Point(int,int)
lateinit isLateinit=true, javaField=late
lazy javaField=lazyProp$delegate
```
| Переход | Как |
|---|---|
| `KClass` → `Class` | `kc.java` |
| `Class` → `KClass` | `cls.kotlin` |
| `KFunction` → `Method` | `fn.javaMethod` |
| `KFunction` → `Constructor` | `fn.javaConstructor` |
| `KProperty` → `Field` | `prop.javaField` |
| `KProperty` → геттер/сеттер | `prop.javaGetter`, `prop.javaSetter` |
| `Method` → `KFunction` | `method.kotlinFunction` |
| `Field` → `KProperty` | `field.kotlinProperty` |
Поле делегированного свойства называется `имя$delegate` - это важно для библиотек, которые ищут поля по имени. Подробнее: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
### Что Java Reflection не видит
```kotlin
class Visibility {
private val hidden = 1
internal val internalProp = 2
val open = 3
}
fun main() {
println("java методы=${Visibility::class.java.declaredMethods.map { it.name }.sorted()}")
println("java поля=${Visibility::class.java.declaredFields.map { it.name }.sorted()}")
}
```
```
java методы=[getInternalProp$main, getOpen]
java поля=[hidden, internalProp, open]
```
Через Java API видны только геттеры (`private val` вообще не имеет метода), а `internal` превращается в `public` с манглированным именем `getInternalProp$main`. Kotlin-рефлексия показывает исходную картину: три свойства с видимостями `PRIVATE`, `INTERNAL`, `PUBLIC`.
## Практический пример: маппинг
```kotlin
import kotlin.reflect.KClass
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
data class Config(val host: String, val port: Int, val debug: Boolean)
fun toMap(obj: Any): Map<String, Any?> =
obj::class.memberProperties.associate { it.name to it.call(obj) }
fun <T : Any> fromMap(kClass: KClass<T>, map: Map<String, Any?>): T {
val ctor = requireNonNull(kClass.primaryConstructor)
val args = ctor.parameters
.filter { map.containsKey(it.name) || !it.isOptional }
.associateWith { map[it.name] }
return ctor.callBy(args)
}
private fun <T> requireNonNull(value: T?): T =
requireNotNull(value) { "у класса нет первичного конструктора" }
fun main() {
val config = Config("localhost", 8080, true)
println(toMap(config).toSortedMap())
val restored = fromMap(
Config::class,
mapOf("host" to "example.com", "port" to 443, "debug" to false),
)
println("восстановлено=$restored")
}
```
```
{debug=true, host=localhost, port=8080}
восстановлено=Config(host=example.com, port=443, debug=false)
```
Фильтр по `isOptional` позволяет не передавать параметры со значениями по умолчанию - этого достаточно для простого маппера конфигурации. Для сериализации в продакшене лучше взять `kotlinx.serialization`: она генерирует код на этапе компиляции и работает без `kotlin-reflect`.
## Производительность
Измерено на 200 000 обращений к свойству `data class`:
```
прямой доступ мс=2
через рефлексию мс=9
замедление раз=3
```
Само обращение медленнее в несколько раз - для большинства задач приемлемо. Дороже стоит **первое** обращение к метаданным:
```
первое обращение к memberProperties мс=158
второе обращение мкс=16
```
158 миллисекунд против 16 микросекунд: `kotlin-reflect` при первом запросе разбирает аннотацию `@Metadata` класса и строит модель. Отсюда простое правило: результаты `memberProperties` и `primaryConstructor` нужно кешировать, а не запрашивать в цикле.
Для многих задач рефлексия вообще не нужна:
| Задача | Альтернатива |
|---|---|
| сериализация | `kotlinx.serialization` - генерация в компиляции |
| DI | ручная сборка или KSP-генерация |
| валидация по аннотациям | KSP-процессор |
| копирование с изменением | `copy()` у `data class` |
| вызов по имени | `Map<String, () -> Unit>` |
| проверка типа | `is` и `when` по `sealed` |
Подробнее: [KSP](../Инструментарий/KSP.md)
> [!info] Отличия от Java
> | Java Reflection | kotlin.reflect |
> |---|---|
> | `Class<T>` | `KClass<T>` |
> | `Field` + `Method` раздельно | `KProperty` как единое целое |
> | `Method` | `KFunction` |
> | в JDK, доступна всегда | отдельная зависимость `kotlin-reflect` |
> | значения по умолчанию не видны | `callBy` их учитывает |
> | nullability не видна | `KType.isMarkedNullable` |
> | аргументы типов стёрты | `KType.arguments` из метаданных |
> | `setAccessible(true)` | `isAccessible = true` |
> | `getSuperclass()` | `supertypes`, `superclasses` |
> | подклассы `sealed` не перечислить | `sealedSubclasses` |
>
> **Свойство - первичная сущность, а не пара «поле + методы».** В Java `private String name` с геттером и сеттером - это `Field` и два `Method`, никак формально не связанные: код, обходящий бин, вынужден угадывать соответствие по именам (`getName` → `name`) и спотыкаться на `isActive` для `boolean`. `KProperty` представляет свойство целиком, с `get`, `set` (у `KMutableProperty`), типом и видимостью; переходы к Java-представлению есть, но нужны редко. Подробнее: [Свойства](../ООП/Свойства.md)
>
> **Метаданные Kotlin хранят то, чего в байт-коде нет.** Компилятор пишет в класс аннотацию `@Metadata` с описанием исходной структуры: nullability, аргументы типов, `suspend`, значения по умолчанию, `internal`-видимость, имена параметров. Java Reflection этого не видит и показывает результат компиляции - `getInternalProp$main` вместо `internal val internalProp`, `Object f(int, Continuation)` вместо `suspend fun f(x: Int): String`. Цена - 3 МБ `kotlin-reflect` и разбор метаданных при первом обращении. Подробнее: [Kotlin в байт-коде](../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
>
> **Рефлексия не входит в стандартную библиотеку, и это осознанное решение.** В Java рефлексия доступна в любой программе - её используют Spring, Jackson, Hibernate, и платить за неё приходится всем. Kotlin целится и в Android с Kotlin/Native, где 3 МБ и динамический разбор метаданных неприемлемы, поэтому полная рефлексия вынесена в отдельный артефакт и на Native отсутствует вовсе. Это подталкивает к генерации кода: `kotlinx.serialization` и KSP решают те же задачи в компиляции. Подробнее: [KSP](../Инструментарий/KSP.md)
>
> **`callBy` возможен только благодаря метаданным.** Значения по умолчанию в Java-байт-коде превращаются в набор перегрузок или в синтетический метод с битовой маской - восстановить «вызови конструктор, остальное по умолчанию» через `Constructor.newInstance` нельзя. `callBy` принимает карту известных параметров и подставляет дефолты сам, что делает рефлективное создание объектов из `Map` практичным.
>
> Сравнить: [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md)
> [!warning] Подводные камни
> **Без `kotlin-reflect` в classpath большая часть API падает в рантайме.** `KotlinReflectionNotSupportedError` возникает при обращении к `memberProperties`, `primaryConstructor`, `sealedSubclasses` - компилятор ошибки не покажет, потому что объявления берутся из stdlib. Добавляйте `implementation(kotlin("reflect"))`.
>
> **Первое обращение к метаданным класса стоит ~150 мс.** Измерено: `memberProperties` впервые - 158 мс, повторно - 16 мкс. В цикле или на горячем пути это катастрофа. Кешируйте `KClass`-производные в `val` на уровне объекта или в `Map`.
>
> **`memberProperties` включает унаследованные, `declaredMemberProperties` - нет.** Путаница этих двух даёт либо лишние поля родителя при сериализации, либо потерю унаследованных. Выбирайте осознанно.
>
> **Доступ к `private` без `isAccessible = true` бросает `IllegalCallableAccessException`.** Это отдельный тип, не `IllegalAccessException` из Java - обработчики, ловящие только Java-исключение, его пропустят.
>
> **`isAccessible = true` может не сработать на JDK 17+.** Строгая инкапсуляция модулей блокирует доступ к внутренностям JDK и закрытых модулей; для своего кода ограничений нет, для чужих библиотек может потребоваться `--add-opens`.
>
> **У локальных и анонимных классов `qualifiedName` равен `null`.** У анонимного объекта `simpleName` тоже `null` - проверено. Код вида `kc.qualifiedName!!` падает именно на них, причём обычно в тестах, где анонимные объекты используются чаще.
>
> **Дженерики стираются, и фактический аргумент типа у значения недоступен.** `list::class.typeParameters` даёт `[E]`, а не `[String]`. Сохранить тип можно только через `reified` в момент компиляции или передавая `KType` параметром. Подробнее: [Reified и inline](../Дженерики/Reified%20и%20inline.md)
>
> **`KType` отражает объявленный тип, а не фактический.** У свойства типа `Any`, в котором лежит `String`, `returnType` останется `kotlin.Any` - для определения фактического нужен `value::class`.
>
> **`call` игнорирует значения по умолчанию, `callBy` - нет.** `ctor.call("a")` на конструкторе с тремя параметрами бросит `IllegalArgumentException` даже если два имеют дефолты. Для частичных аргументов нужен `callBy` с `Map<KParameter, Any?>`.
>
> **`callBy` с `null` для необязательного параметра передаст именно `null`, а не дефолт.** Ключ, присутствующий в карте, считается заданным; чтобы использовать значение по умолчанию, параметр нужно **не включать** в карту вовсе.
>
> **Поле делегированного свойства называется `имя$delegate`.** `javaField?.name` для `val x by lazy { }` даст `x$delegate`, а не `x` - код, ищущий поля по имени свойства, их не найдёт.
>
> **`getDelegate()` требует `isAccessible = true` и возвращает делегат, а не значение.** Для `by lazy` это `Lazy<T>`, чей `toString` до инициализации - «Lazy value not initialized yet.»; попытка привести его к типу значения даст `ClassCastException`.
>
> **`internal` в Java-представлении становится `public` с манглированным именем.** `getInternalProp$main` - проверено; обход через `Class.getDeclaredMethods` покажет именно это имя, а не `getInternalProp`.
>
> **`objectInstance` равен `null` для обычных классов.** Для проверки «это `object`?» нужен `kc.objectInstance != null`, а не наличие свойства.
>
> **Рефлексия обходит проверки null-безопасности.** `prop.set(obj, null)` через `KMutableProperty` с типом `String` запишет `null` в непустое свойство: компилятор не участвует, а проверки `Intrinsics` стоят только на входе в функции. Дальше код упадёт NPE в неожиданном месте. Подробнее: [Null-безопасность](../Синтаксис%20и%20типы/Null-безопасность/Null-безопасность.md)
>
> **Рефлексия ломается при обфускации.** R8 и ProGuard переименовывают классы и члены; код, ищущий свойство по строковому имени, перестаёт работать в релизной сборке и работает в отладочной. Нужны правила `-keep`.
>
> **Kotlin/Native и Kotlin/JS не поддерживают полную рефлексию.** `kotlin-reflect` существует только для JVM; в Multiplatform-коде доступны лишь `::class.simpleName` и подобное. Общий код на рефлексии не построить.
Далее: [Аннотации](Аннотации.md)