--- 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` | ### 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 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 = ::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` | свойство без получателя: `::topLevelVal`, `obj::prop` | | `KProperty1` | свойство класса: `User::name` | | `KProperty2` | свойство-расширение с двумя получателями | | `KMutableProperty1` | то же для `var` - добавляет `set` | | `KFunction` | функция; `KFunction1`, `KFunction2`, ... по арности | | `KParameter` | параметр: `name`, `type`, `isOptional`, `kind` | | `KType` | тип с аргументами и `isMarkedNullable` | `call` и `callBy` различаются принципиально: `call` требует все аргументы по порядку, `callBy` принимает `Map` и **учитывает значения по умолчанию** для отсутствующих. Значения по умолчанию в байт-коде не видны, поэтому через 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> = emptyList() } fun nullableParam(x: String?) = x fun main() { val t = typeOf>>() 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> classifier=class kotlin.collections.List аргументы=[kotlin.collections.Map] тип свойства=kotlin.collections.List> вложенный аргумент=kotlin.collections.Map nullable=true ``` `KType` сохраняет аргументы типов и признак nullable - эта информация берётся из метаданных Kotlin, а не из байт-кода. Но это **статическая** информация: тип объявления, а не фактический тип значения. ```kotlin fun main() { val list: List = 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 = obj::value prop.isAccessible = true println("делегат до чтения=${prop.getDelegate()}") println("значение=${obj.value}") println("делегат после=${prop.getDelegate()}") } ``` ``` делегат до чтения=Lazy value not initialized yet. значение=вычислено делегат после=вычислено ``` `getDelegate()` возвращает сам объект-делегат, а не значение свойства - для `by lazy` это `Lazy`, чей `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 = obj::class.memberProperties.associate { it.name to it.call(obj) } fun fromMap(kClass: KClass, map: Map): 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 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 Unit>` | | проверка типа | `is` и `when` по `sealed` | Подробнее: [KSP](../Инструментарий/KSP.md) > [!info] Отличия от Java > | Java Reflection | kotlin.reflect | > |---|---| > | `Class` | `KClass` | > | `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`. > > **`callBy` с `null` для необязательного параметра передаст именно `null`, а не дефолт.** Ключ, присутствующий в карте, считается заданным; чтобы использовать значение по умолчанию, параметр нужно **не включать** в карту вовсе. > > **Поле делегированного свойства называется `имя$delegate`.** `javaField?.name` для `val x by lazy { }` даст `x$delegate`, а не `x` - код, ищущий поля по имени свойства, их не найдёт. > > **`getDelegate()` требует `isAccessible = true` и возвращает делегат, а не значение.** Для `by lazy` это `Lazy`, чей `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)