Files
obsidian/Kotlin/Рефлексия/Рефлексия.md
T

506 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)