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

29 KiB
Raw Blame History

title, description, tags
title description tags
Рефлексия KClass, KProperty, KFunction, kotlin-reflect, ссылки на члены, интероп с Java Reflection
kotlin
reflection

Рефлексия - доступ к структуре программы во время выполнения: какие у класса свойства, какие параметры у функции, как вызвать то, что не известно на этапе компиляции. Kotlin даёт собственный API (kotlin.reflect) поверх Java Reflection, потому что в байт-коде не видно ни свойств, ни suspend, ни значений по умолчанию.

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-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

Подключается отдельно:

// build.gradle.kts
dependencies {
    implementation(kotlin("reflect"))
}

kotlin-reflect весит около 3 МБ и тянет собственный парсер метаданных - для Android и serverless это значимо. Подробнее: Gradle и Kotlin DSL

KClass

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

Свойство 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

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

Ссылки на члены

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 не воспроизвести. Подробнее: Функции

Приватные члены

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+ она может упираться в строгую инкапсуляцию модулей для классов стандартной библиотеки; для собственного кода работает всегда.

Типы

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, а не из байт-кода. Но это статическая информация: тип объявления, а не фактический тип значения.

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

Делегаты

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». Это единственный способ узнать состояние ленивого свойства, не вычисляя его. Подробнее: Делегирование

Интероп с Java Reflection

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 в байт-коде

Что Java Reflection не видит

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.

Практический пример: маппинг

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

[!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-представлению есть, но нужны редко. Подробнее: Свойства

Метаданные 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 в байт-коде

Рефлексия не входит в стандартную библиотеку, и это осознанное решение. В Java рефлексия доступна в любой программе - её используют Spring, Jackson, Hibernate, и платить за неё приходится всем. Kotlin целится и в Android с Kotlin/Native, где 3 МБ и динамический разбор метаданных неприемлемы, поэтому полная рефлексия вынесена в отдельный артефакт и на Native отсутствует вовсе. Это подталкивает к генерации кода: kotlinx.serialization и KSP решают те же задачи в компиляции. Подробнее: KSP

callBy возможен только благодаря метаданным. Значения по умолчанию в Java-байт-коде превращаются в набор перегрузок или в синтетический метод с битовой маской - восстановить «вызови конструктор, остальное по умолчанию» через Constructor.newInstance нельзя. callBy принимает карту известных параметров и подставляет дефолты сам, что делает рефлективное создание объектов из Map практичным.

Сравнить: Reflection API в Java

[!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

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-безопасность

Рефлексия ломается при обфускации. R8 и ProGuard переименовывают классы и члены; код, ищущий свойство по строковому имени, перестаёт работать в релизной сборке и работает в отладочной. Нужны правила -keep.

Kotlin/Native и Kotlin/JS не поддерживают полную рефлексию. kotlin-reflect существует только для JVM; в Multiplatform-коде доступны лишь ::class.simpleName и подобное. Общий код на рефлексии не построить.

Далее: Аннотации