Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,653 @@
|
||||
---
|
||||
title: Аннотации
|
||||
description: Объявление аннотаций, цели, retention, use-site targets, чтение через рефлексию, аннотации интеропа
|
||||
tags:
|
||||
- kotlin
|
||||
- annotations
|
||||
- reflection
|
||||
---
|
||||
|
||||
Аннотация - метаданные, прикреплённые к объявлению. Сама по себе она ничего не делает: смысл ей придаёт тот, кто её читает - рефлексия в рантайме, процессор аннотаций в компиляции или сам компилятор.
|
||||
|
||||
```kotlin
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Table(val name: String, val schema: String = "public")
|
||||
|
||||
@Table("users")
|
||||
class User(val id: Int, val name: String)
|
||||
|
||||
fun main() {
|
||||
val table = User::class.findAnnotation<Table>()
|
||||
println("таблица=${table?.name}, схема=${table?.schema}")
|
||||
}
|
||||
```
|
||||
```
|
||||
таблица=users, схема=public
|
||||
```
|
||||
|
||||
## Объявление
|
||||
```kotlin
|
||||
annotation class Simple
|
||||
|
||||
annotation class WithParams(
|
||||
val name: String,
|
||||
val priority: Int = 0,
|
||||
val tags: Array<String> = [],
|
||||
)
|
||||
|
||||
fun main() {
|
||||
println("объявлены")
|
||||
}
|
||||
```
|
||||
```
|
||||
объявлены
|
||||
```
|
||||
Правила для аннотаций жёстче, чем для обычных классов:
|
||||
- нет тела, нет функций и состояния - только параметры конструктора;
|
||||
- все параметры - `val`;
|
||||
- типы параметров ограничены: примитивы, `String`, `KClass`, `enum`, другая аннотация, массивы перечисленного;
|
||||
- нельзя `null` в качестве значения по умолчанию - сам тип не может быть nullable.
|
||||
|
||||
## Цели - @Target
|
||||
```kotlin
|
||||
@Target(
|
||||
AnnotationTarget.CLASS,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.PROPERTY,
|
||||
AnnotationTarget.VALUE_PARAMETER,
|
||||
)
|
||||
annotation class Marker
|
||||
|
||||
@Marker
|
||||
class Annotated(@Marker val prop: Int) {
|
||||
@Marker
|
||||
fun method() = Unit
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println("применено к четырём местам")
|
||||
}
|
||||
```
|
||||
```
|
||||
применено к четырём местам
|
||||
```
|
||||
| Цель | Куда |
|
||||
|---|---|
|
||||
| `CLASS` | класс, интерфейс, `object`, аннотация |
|
||||
| `FUNCTION` | функция, не конструктор |
|
||||
| `PROPERTY` | свойство (только Kotlin-код) |
|
||||
| `FIELD` | backing field свойства |
|
||||
| `VALUE_PARAMETER` | параметр функции или конструктора |
|
||||
| `CONSTRUCTOR` | конструктор |
|
||||
| `PROPERTY_GETTER`, `PROPERTY_SETTER` | аксессоры |
|
||||
| `LOCAL_VARIABLE` | локальная переменная |
|
||||
| `TYPE` | использование типа: `List<@Marker String>` |
|
||||
| `TYPE_PARAMETER` | параметр типа |
|
||||
| `EXPRESSION` | выражение (требует `SOURCE`) |
|
||||
| `FILE` | файл, через `@file:` |
|
||||
| `ANNOTATION_CLASS` | мета-аннотация |
|
||||
|
||||
Без `@Target` аннотация применима почти везде, кроме `TYPE`, `EXPRESSION`, `FILE` и `TYPE_PARAMETER` - их нужно указывать явно.
|
||||
|
||||
## Хранение - @Retention
|
||||
```kotlin
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Src
|
||||
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Bin
|
||||
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Run
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class NoRetention
|
||||
|
||||
@Src @Bin @Run
|
||||
class All
|
||||
|
||||
@NoRetention
|
||||
class Default
|
||||
|
||||
fun main() {
|
||||
println("All=${All::class.annotations.map { it.annotationClass.simpleName }}")
|
||||
println("Default=${Default::class.annotations.map { it.annotationClass.simpleName }}")
|
||||
}
|
||||
```
|
||||
```
|
||||
All=[Run]
|
||||
Default=[NoRetention]
|
||||
```
|
||||
| Retention | В байт-коде | Видна рефлексии |
|
||||
|---|---|---|
|
||||
| `SOURCE` | нет | нет |
|
||||
| `BINARY` | да | нет |
|
||||
| `RUNTIME` (по умолчанию) | да | да |
|
||||
|
||||
Ключевое отличие от Java: **по умолчанию `RUNTIME`**, а не `CLASS`. Поэтому `@NoRetention` без явного указания видна через рефлексию - в Java аналогичная аннотация была бы невидима. Для аннотаций, которые читает только KSP или линтер, стоит явно ставить `SOURCE`: это убирает их из байт-кода.
|
||||
|
||||
## Use-site targets
|
||||
Свойство в Kotlin компилируется в несколько Java-элементов: поле, геттер, иногда сеттер, а для параметра конструктора ещё и параметр. Аннотация должна попасть на конкретный из них - для этого есть префиксы.
|
||||
|
||||
```kotlin
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.javaConstructor
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import kotlin.reflect.jvm.javaGetter
|
||||
import kotlin.reflect.jvm.javaSetter
|
||||
|
||||
@Target(
|
||||
AnnotationTarget.PROPERTY,
|
||||
AnnotationTarget.FIELD,
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.PROPERTY_SETTER,
|
||||
AnnotationTarget.VALUE_PARAMETER,
|
||||
)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Mark(val where: String)
|
||||
|
||||
class Targets(
|
||||
@Mark("без префикса") val implicit: Int,
|
||||
@field:Mark("field") val onField: Int,
|
||||
@get:Mark("getter") val onGetter: Int,
|
||||
@property:Mark("property") val onProperty: Int,
|
||||
) {
|
||||
@set:Mark("setter")
|
||||
var mutable: Int = 0
|
||||
}
|
||||
|
||||
fun main() {
|
||||
for (p in Targets::class.memberProperties.sortedBy { it.name }) {
|
||||
val prop = p.annotations.filterIsInstance<Mark>().map { it.where }
|
||||
val field = p.javaField?.annotations?.filterIsInstance<Mark>()?.map { it.where }.orEmpty()
|
||||
val getter = p.javaGetter?.annotations?.filterIsInstance<Mark>()?.map { it.where }.orEmpty()
|
||||
println("${p.name}: property=$prop field=$field getter=$getter")
|
||||
}
|
||||
val ctor = Targets::class.primaryConstructor!!.javaConstructor!!
|
||||
ctor.parameterAnnotations.forEachIndexed { i, anns ->
|
||||
println("параметр $i: ${anns.filterIsInstance<Mark>().map { it.where }}")
|
||||
}
|
||||
val mutable = Targets::class.memberProperties.first { it.name == "mutable" }
|
||||
val setter = (mutable as KMutableProperty1<*, *>).javaSetter
|
||||
println("setter=${setter?.annotations?.filterIsInstance<Mark>()?.map { it.where }}")
|
||||
}
|
||||
```
|
||||
```
|
||||
implicit: property=[без префикса] field=[] getter=[]
|
||||
mutable: property=[] field=[] getter=[]
|
||||
onField: property=[] field=[field] getter=[]
|
||||
onGetter: property=[] field=[] getter=[getter]
|
||||
onProperty: property=[property] field=[] getter=[]
|
||||
параметр 0: [без префикса]
|
||||
параметр 1: []
|
||||
параметр 2: []
|
||||
параметр 3: []
|
||||
setter=[setter]
|
||||
```
|
||||
| Префикс | Куда попадёт |
|
||||
|---|---|
|
||||
| `@property:` | свойство (видно только Kotlin-рефлексии) |
|
||||
| `@field:` | backing field |
|
||||
| `@get:` | геттер |
|
||||
| `@set:` | сеттер |
|
||||
| `@param:` | параметр конструктора |
|
||||
| `@setparam:` | параметр сеттера |
|
||||
| `@receiver:` | получатель функции-расширения |
|
||||
| `@delegate:` | поле делегата |
|
||||
| `@file:` | файл, до объявления `package` |
|
||||
| `@all:` | все применимые цели сразу |
|
||||
|
||||
### Правило выбора по умолчанию
|
||||
Без префикса аннотация применяется к **первой подходящей** цели из списка, в порядке: `param` → `property` → `field`. Это проверяется легко:
|
||||
```kotlin
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.javaConstructor
|
||||
import kotlin.reflect.jvm.javaField
|
||||
|
||||
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Multi
|
||||
|
||||
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.FIELD)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class PropOrField
|
||||
|
||||
@Target(AnnotationTarget.FIELD)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class FieldOnly
|
||||
|
||||
class C(
|
||||
@Multi val a: Int,
|
||||
@PropOrField val b: Int,
|
||||
@FieldOnly val c: Int,
|
||||
)
|
||||
|
||||
fun report(name: String) {
|
||||
val p = C::class.memberProperties.first { it.name == name }
|
||||
val onProperty = p.annotations.map { it.annotationClass.simpleName }
|
||||
val onField = p.javaField?.annotations?.map { it.annotationClass.simpleName }.orEmpty()
|
||||
val index = C::class.primaryConstructor!!.parameters.indexOfFirst { it.name == name }
|
||||
val onParam = C::class.primaryConstructor!!.javaConstructor!!
|
||||
.parameterAnnotations[index].map { it.annotationClass.simpleName }
|
||||
println("$name: property=$onProperty field=$onField param=$onParam")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
report("a")
|
||||
report("b")
|
||||
report("c")
|
||||
}
|
||||
```
|
||||
```
|
||||
a: property=[Multi] field=[] param=[Multi]
|
||||
b: property=[PropOrField] field=[] param=[]
|
||||
c: property=[] field=[FieldOnly] param=[]
|
||||
```
|
||||
Обратите внимание на `a`: аннотация, применимая и к параметру, и к свойству, попала **на оба** - на параметр конструктора и на Kotlin-свойство (но не на поле). Именно здесь возникают проблемы с Java-библиотеками: Jackson или Hibernate читают поля через Java Reflection, а там аннотации нет - нужен явный `@field:`.
|
||||
|
||||
### @all
|
||||
```kotlin
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.reflect.jvm.javaConstructor
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import kotlin.reflect.jvm.javaGetter
|
||||
|
||||
@Target(
|
||||
AnnotationTarget.PROPERTY,
|
||||
AnnotationTarget.FIELD,
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
AnnotationTarget.VALUE_PARAMETER,
|
||||
)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Mark
|
||||
|
||||
class WithAll(@all:Mark val x: Int)
|
||||
|
||||
fun main() {
|
||||
val p = WithAll::class.memberProperties.first()
|
||||
println("property=${p.annotations.size}")
|
||||
println("field=${p.javaField?.annotations?.size}")
|
||||
println("getter=${p.javaGetter?.annotations?.size}")
|
||||
val params = WithAll::class.primaryConstructor!!.javaConstructor!!.parameterAnnotations
|
||||
println("param=${params[0].size}")
|
||||
}
|
||||
```
|
||||
```
|
||||
property=1
|
||||
field=1
|
||||
getter=1
|
||||
param=1
|
||||
```
|
||||
`@all:` - решение давней проблемы: раньше приходилось писать `@field:Mark @get:Mark @param:Mark` подряд. В Kotlin 2.4 работает без дополнительных флагов компилятора.
|
||||
|
||||
## Параметры аннотаций
|
||||
```kotlin
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
|
||||
interface Validator
|
||||
class NotBlank : Validator
|
||||
class MaxLength : Validator
|
||||
|
||||
enum class Mode { FAST, SAFE }
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Validators(val value: Array<KClass<out Validator>>)
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Levels(val value: IntArray, val names: Array<String>)
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Nested(val inner: Levels)
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class WithEnum(val mode: Mode)
|
||||
|
||||
@Validators([NotBlank::class, MaxLength::class])
|
||||
@Levels(value = [1, 2, 3], names = ["low", "mid", "high"])
|
||||
@Nested(Levels([9], ["x"]))
|
||||
@WithEnum(Mode.SAFE)
|
||||
class Configured
|
||||
|
||||
fun main() {
|
||||
val kc = Configured::class
|
||||
println("классы=${kc.findAnnotation<Validators>()!!.value.map { it.simpleName }}")
|
||||
val levels = kc.findAnnotation<Levels>()!!
|
||||
println("IntArray=${levels.value.toList()} names=${levels.names.toList()}")
|
||||
println("вложенная=${kc.findAnnotation<Nested>()!!.inner.value.toList()}")
|
||||
println("enum=${kc.findAnnotation<WithEnum>()!!.mode}")
|
||||
}
|
||||
```
|
||||
```
|
||||
классы=[NotBlank, MaxLength]
|
||||
IntArray=[1, 2, 3] names=[low, mid, high]
|
||||
вложенная=[9]
|
||||
enum=SAFE
|
||||
```
|
||||
Массивы задаются литералом `[a, b]` - старый синтаксис `arrayOf(a, b)` тоже работает, но литерал короче. Классы передаются как `KClass`, то есть `Foo::class`, а не `Foo::class.java`.
|
||||
|
||||
## Значения по умолчанию и equals
|
||||
```kotlin
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class HasDefaults(val a: Int = 1, val b: String = "x", val c: IntArray = [1, 2])
|
||||
|
||||
@HasDefaults
|
||||
class WithDefaults
|
||||
|
||||
@HasDefaults(a = 5)
|
||||
class Partial
|
||||
|
||||
fun main() {
|
||||
val d = WithDefaults::class.findAnnotation<HasDefaults>()!!
|
||||
println("дефолты a=${d.a} b=${d.b} c=${d.c.toList()}")
|
||||
val p = Partial::class.findAnnotation<HasDefaults>()!!
|
||||
println("частично a=${p.a} b=${p.b}")
|
||||
println("toString=$p")
|
||||
println("равенство=${d == WithDefaults::class.findAnnotation<HasDefaults>()}")
|
||||
}
|
||||
```
|
||||
```
|
||||
дефолты a=1 b=x c=[1, 2]
|
||||
частично a=5 b=x
|
||||
toString=@HasDefaults(a=5, b="x", c={1, 2})
|
||||
равенство=true
|
||||
```
|
||||
Экземпляры аннотаций сравниваются по значениям параметров, а `toString` даёт читаемое представление с подставленными дефолтами - удобно для отладки.
|
||||
|
||||
## Повторяемые аннотации
|
||||
```kotlin
|
||||
import kotlin.reflect.full.findAnnotations
|
||||
|
||||
@Repeatable
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Tag(val value: String)
|
||||
|
||||
@Tag("a")
|
||||
@Tag("b")
|
||||
class Tagged
|
||||
|
||||
fun main() {
|
||||
println("все теги=${Tagged::class.findAnnotations<Tag>().map { it.value }}")
|
||||
println("через annotations=${Tagged::class.annotations.filterIsInstance<Tag>().map { it.value }}")
|
||||
}
|
||||
```
|
||||
```
|
||||
все теги=[a, b]
|
||||
через annotations=[a, b]
|
||||
```
|
||||
В Java для повторяемых аннотаций нужен явный контейнер (`@Repeatable(Tags.class)` плюс класс `Tags` с массивом). В Kotlin достаточно `@Repeatable`, а для чтения есть `findAnnotations<T>()`, возвращающий список.
|
||||
|
||||
## Чтение через рефлексию
|
||||
```kotlin
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.hasAnnotation
|
||||
import kotlin.reflect.full.memberProperties
|
||||
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Table(val name: String)
|
||||
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Column(val name: String, val nullable: Boolean = false)
|
||||
|
||||
@Table("users")
|
||||
class User(
|
||||
@Column("user_id") val id: Int,
|
||||
@Column("full_name", nullable = true) val name: String?,
|
||||
)
|
||||
|
||||
fun main() {
|
||||
val kc = User::class
|
||||
println("hasAnnotation=${kc.hasAnnotation<Table>()}")
|
||||
println("findAnnotation=${kc.findAnnotation<Table>()?.name}")
|
||||
for (p in kc.memberProperties.sortedBy { it.name }) {
|
||||
val column = p.findAnnotation<Column>()
|
||||
println("${p.name}: column=${column?.name} nullable=${column?.nullable}")
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
hasAnnotation=true
|
||||
findAnnotation=users
|
||||
id: column=user_id nullable=false
|
||||
name: column=full_name nullable=true
|
||||
```
|
||||
| Функция | Что возвращает |
|
||||
|---|---|
|
||||
| `annotations` | список всех `RUNTIME`-аннотаций элемента |
|
||||
| `findAnnotation<T>()` | первую аннотацию типа `T` или `null` |
|
||||
| `findAnnotations<T>()` | все аннотации типа `T` (для `@Repeatable`) |
|
||||
| `hasAnnotation<T>()` | есть ли аннотация |
|
||||
| `annotationClass` | `KClass` самой аннотации |
|
||||
|
||||
Эти функции есть у `KClass`, `KProperty`, `KFunction` и `KParameter` - интерфейс единый. Подробнее: [Рефлексия](Рефлексия.md)
|
||||
|
||||
## Практический пример: валидация
|
||||
```kotlin
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.jvm.isAccessible
|
||||
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class NotBlank(val message: String = "не должно быть пустым")
|
||||
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Range(val min: Int, val max: Int)
|
||||
|
||||
data class Form(
|
||||
@NotBlank val name: String,
|
||||
@NotBlank("email обязателен") val email: String,
|
||||
@Range(18, 120) val age: Int,
|
||||
)
|
||||
|
||||
fun validate(obj: Any): List<String> {
|
||||
val errors = mutableListOf<String>()
|
||||
for (p in obj::class.memberProperties) {
|
||||
p.isAccessible = true
|
||||
val value = p.call(obj)
|
||||
p.findAnnotation<NotBlank>()?.let { rule ->
|
||||
if ((value as? String).isNullOrBlank()) errors += "${p.name}: ${rule.message}"
|
||||
}
|
||||
p.findAnnotation<Range>()?.let { rule ->
|
||||
val number = value as? Int ?: return@let
|
||||
if (number !in rule.min..rule.max) {
|
||||
errors += "${p.name}: должно быть от ${rule.min} до ${rule.max}"
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.sorted()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(validate(Form("Иван", "ivan@example.com", 30)))
|
||||
println(validate(Form("", "", 5)))
|
||||
}
|
||||
```
|
||||
```
|
||||
[]
|
||||
[age: должно быть от 18 до 120, email: email обязателен, name: не должно быть пустым]
|
||||
```
|
||||
Это рабочая схема, но она платит рефлексией в рантайме: разбор метаданных при первом обращении и потеря проверок на этапе компиляции. Тот же результат даёт KSP-процессор, генерирующий код валидации при сборке. Подробнее: [KSP](../Инструментарий/KSP.md)
|
||||
|
||||
## Аннотации компилятора
|
||||
Часть аннотаций обрабатывает сам компилятор - они меняют генерируемый код или проверки.
|
||||
|
||||
| Аннотация | Что делает |
|
||||
|---|---|
|
||||
| `@Deprecated` | предупреждение или ошибка при использовании |
|
||||
| `@Suppress("NAME")` | подавляет предупреждение в области применения |
|
||||
| `@JvmName` | имя в байт-коде |
|
||||
| `@JvmStatic` | статический метод для членов `companion object` |
|
||||
| `@JvmOverloads` | перегрузки для параметров со значениями по умолчанию |
|
||||
| `@JvmField` | публичное поле вместо геттера |
|
||||
| `@JvmInline` | обязательна для `value class` |
|
||||
| `@Throws` | добавляет `throws` в сигнатуру для Java |
|
||||
| `@OptIn`, `@RequiresOptIn` | механизм согласия на нестабильный API |
|
||||
| `@PublishedApi` | доступ к `internal` из `public inline` |
|
||||
| `@Volatile`, `@Synchronized` | то же, что в Java |
|
||||
| `@Transient` | исключение из сериализации |
|
||||
| `@DslMarker` | ограничение области в DSL |
|
||||
| `@BuilderInference` | вывод типа в билдерах |
|
||||
|
||||
```kotlin
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
|
||||
@Deprecated("используйте new()", ReplaceWith("new()"), DeprecationLevel.WARNING)
|
||||
fun old() = 1
|
||||
|
||||
fun new() = 2
|
||||
|
||||
fun main() {
|
||||
val d = ::old.findAnnotation<Deprecated>()
|
||||
println("message=${d?.message}")
|
||||
println("level=${d?.level}")
|
||||
println("replaceWith=${d?.replaceWith?.expression}")
|
||||
}
|
||||
```
|
||||
```
|
||||
message=используйте new()
|
||||
level=WARNING
|
||||
replaceWith=new()
|
||||
```
|
||||
`ReplaceWith` понимает IDE: она предлагает автозамену вызова. `DeprecationLevel.ERROR` делает использование ошибкой компиляции, `HIDDEN` убирает объявление из автодополнения, сохраняя бинарную совместимость.
|
||||
|
||||
Аннотации интеропа меняют байт-код заметно:
|
||||
```kotlin
|
||||
@file:JvmName("InteropUtils")
|
||||
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.Modifier
|
||||
|
||||
class Interop {
|
||||
@Throws(IOException::class)
|
||||
fun mayThrow() = Unit
|
||||
|
||||
@JvmField
|
||||
val field = 1
|
||||
|
||||
@get:JvmName("fetchValue")
|
||||
val value = 2
|
||||
|
||||
@JvmOverloads
|
||||
fun withDefaults(a: Int, b: Int = 1, c: Int = 2) = a + b + c
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun staticFn() = 3
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val c = Interop::class.java
|
||||
println("методы=${c.declaredMethods.map { it.name }.sorted()}")
|
||||
println("поля=${c.declaredFields.map { it.name }.sorted()}")
|
||||
println("throws=${c.getDeclaredMethod("mayThrow").exceptionTypes.map { it.simpleName }}")
|
||||
println("статические=${c.declaredMethods.filter { Modifier.isStatic(it.modifiers) }.map { it.name }.sorted()}")
|
||||
}
|
||||
```
|
||||
```
|
||||
методы=[fetchValue, mayThrow, staticFn, withDefaults, withDefaults, withDefaults, withDefaults$default]
|
||||
поля=[Companion, field, value]
|
||||
throws=[IOException]
|
||||
статические=[staticFn, withDefaults$default]
|
||||
```
|
||||
`@JvmOverloads` дал три перегрузки `withDefaults`, `@get:JvmName` переименовал геттер в `fetchValue`, `@JvmField` превратил свойство в поле без геттера. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||||
|
||||
## Аннотации для KSP
|
||||
Аннотация, предназначенная процессору, не нужна в рантайме:
|
||||
```kotlin
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
annotation class GenerateBuilder
|
||||
|
||||
@GenerateBuilder
|
||||
data class Person(val name: String, val age: Int)
|
||||
|
||||
fun main() {
|
||||
println("в рантайме=${Person::class.annotations.size}")
|
||||
}
|
||||
```
|
||||
```
|
||||
в рантайме=0
|
||||
```
|
||||
`SOURCE` убирает аннотацию из байт-кода полностью - KSP читает исходники, а не классы. Это уменьшает размер артефакта и исключает случайное чтение рефлексией. Подробнее: [KSP](../Инструментарий/KSP.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `@interface Foo { String value(); }` | `annotation class Foo(val value: String)` |
|
||||
> | `String value() default "x"` | `val value: String = "x"` |
|
||||
> | retention по умолчанию `CLASS` | по умолчанию `RUNTIME` |
|
||||
> | `@Target(ElementType.FIELD)` | `@Target(AnnotationTarget.FIELD)` |
|
||||
> | нет понятия свойства | `@property:`, `@field:`, `@get:`, `@set:` |
|
||||
> | `@Repeatable(Container.class)` + контейнер | просто `@Repeatable` |
|
||||
> | `Foo.class` в параметре | `Foo::class` |
|
||||
> | `{1, 2}` для массива | `[1, 2]` |
|
||||
> | `getAnnotation(Foo.class)` | `findAnnotation<Foo>()` |
|
||||
> | аннотации на файл невозможны | `@file:JvmName(...)` |
|
||||
>
|
||||
> **Use-site targets существуют потому, что свойство - не поле.** В Java `@Column private String name` однозначна: цель одна. В Kotlin `val name: String` порождает поле, геттер и Kotlin-свойство как отдельную сущность в метаданных, а в конструкторе ещё и параметр - аннотация должна выбрать одну цель. По умолчанию выбирается первая применимая в порядке `param` → `property` → `field`, и это главная причина, по которой Java-библиотеки «не видят» аннотацию: Jackson читает поле, а аннотация оказалась на параметре. Префикс `@field:` или новый `@all:` решают проблему. Подробнее: [Свойства](../ООП/Свойства.md)
|
||||
>
|
||||
> **Retention по умолчанию - `RUNTIME`, а не `CLASS`.** Java по умолчанию не включает аннотацию в рантайм, поэтому каждая библиотека обязана ставить `@Retention(RUNTIME)` явно. Kotlin выбрал обратный дефолт - аннотации чаще читают рефлексией, чем процессором. Обратная сторона: аннотация для линтера без явного `SOURCE` попадёт и в байт-код, и в рантайм, увеличивая артефакт.
|
||||
>
|
||||
> **Повторяемость не требует класса-контейнера.** В Java `@Repeatable` обязывает объявить отдельную аннотацию-контейнер с массивом - лишний код, который приходится писать для каждой повторяемой аннотации. Kotlin генерирует контейнер сам; `findAnnotations<T>()` возвращает список, и раскрывать контейнер вручную не нужно.
|
||||
>
|
||||
> **Аннотации на файл - новая цель, которой в Java нет.** `@file:JvmName("Utils")` меняет имя класса-контейнера для функций верхнего уровня, `@file:Suppress` подавляет предупреждения во всём файле. В Java единица объявления - всегда класс, поэтому и цели `FILE` быть не может. Подробнее: [Пакеты и импорты](../Синтаксис%20и%20типы/Базовый%20синтаксис/Пакеты%20и%20импорты.md)
|
||||
>
|
||||
> **Генерация кода вместо рефлексии - предпочтительный путь.** В Java-экосистеме аннотации почти всегда читаются рефлексией: Spring, Jackson, Hibernate строят модель при старте приложения. Kotlin подталкивает к другому: `kotlinx.serialization` и KSP обрабатывают аннотации при компиляции и генерируют код, поэтому `kotlin-reflect` не нужен, старт быстрее, а ошибки видны при сборке. Для Kotlin/Native и Kotlin/JS это единственный работающий вариант. Подробнее: [KSP](../Инструментарий/KSP.md)
|
||||
>
|
||||
> Сравнить: [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Аннотация без префикса может не попасть туда, куда нужно Java-библиотеке.** Проверено: аннотация с целями `VALUE_PARAMETER` и `PROPERTY` на `val` в конструкторе попадает на параметр и Kotlin-свойство, но **не на поле** - Jackson, Hibernate и JPA её не увидят. Нужен `@field:` или `@all:`.
|
||||
>
|
||||
> **`@property:` видна только Kotlin-рефлексии.** В Java-представлении такой аннотации нет вообще: она хранится в метаданных Kotlin. Библиотека на Java Reflection её не найдёт никогда.
|
||||
>
|
||||
> **Retention по умолчанию `RUNTIME` раздувает байт-код.** Аннотации для KSP, линтеров и внутренних проверок стоит помечать `@Retention(AnnotationRetention.SOURCE)` - иначе они остаются в классах и видны рефлексии без необходимости.
|
||||
>
|
||||
> **`BINARY` не видна рефлексии, хотя лежит в байт-коде.** Проверено: из `@Src @Bin @Run` рефлексия вернёт только `Run`. Это частая причина «аннотация есть в коде, а `findAnnotation` даёт `null`».
|
||||
>
|
||||
> **`null` в качестве значения аннотации невозможен.** Типы параметров не могут быть nullable, поэтому «отсутствие значения» кодируют пустой строкой или сигнальной константой. Это ограничение формата аннотаций в JVM, а не Kotlin.
|
||||
>
|
||||
> **Параметры аннотаций ограничены по типам.** `data class`, коллекции, `Duration` и функции использовать нельзя - только примитивы, `String`, `KClass`, `enum`, другая аннотация и массивы из них. Попытка передать `List<String>` вместо `Array<String>` не скомпилируется.
|
||||
>
|
||||
> **`KClass` в параметре, а не `Class`.** `@Validators([NotBlank::class])` - правильно, `NotBlank::class.java` в аннотации не примут. При чтении обратно получается `KClass`, и для Java-API нужен `.java`.
|
||||
>
|
||||
> **`@JvmStatic` работает только в `companion object` и `object`.** На обычном методе класса даёт ошибку компиляции - статических методов у Kotlin-классов не бывает. Подробнее: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
|
||||
>
|
||||
> **`@JvmOverloads` генерирует перегрузки только справа налево.** Для `f(a, b = 1, c = 2)` появятся `f(a)`, `f(a, b)`, `f(a, b, c)` - вызвать `f(a, c)` из Java не получится. Проверено на реальном байт-коде.
|
||||
>
|
||||
> **`@Throws` обязательна для Java-вызывающих.** Kotlin не имеет проверяемых исключений, поэтому без `@Throws` Java-код не сможет написать `catch (IOException e)` - компилятор Java сообщит, что исключение не бросается. Подробнее: [Система исключений](../Исключения/Система%20исключений.md)
|
||||
>
|
||||
> **Аннотации на типах требуют явной цели `TYPE`.** `List<@Marker String>` не скомпилируется, если в `@Target` нет `AnnotationTarget.TYPE` - по умолчанию эта цель не включена.
|
||||
>
|
||||
> **`@Target(AnnotationTarget.EXPRESSION)` работает только с `SOURCE`.** Выражения не существуют в байт-коде, поэтому `RUNTIME` с такой целью даст ошибку компиляции.
|
||||
>
|
||||
> **`@all:` - новая возможность и ломает сборку на старых версиях Kotlin.** В 2.4 работает без флагов, в более ранних версиях требует `-Xannotation-target-all` или недоступна вовсе. Для библиотек с широкой совместимостью пока надёжнее перечислять цели вручную.
|
||||
>
|
||||
> **Чтение аннотаций рефлексией стоит дорого при первом обращении.** Это тот же разбор метаданных, что и у остальной рефлексии: ~150 мс на класс впервые. Кешируйте результат, а лучше переносите обработку в KSP. Подробнее: [Рефлексия](Рефлексия.md)
|
||||
>
|
||||
> **Обфускация ломает аннотации с именами в строках.** `@Column("user_id")` переживёт R8, а вот поиск свойства по имени из аннотации - нет: само свойство переименуется. Нужны правила `-keep`.
|
||||
|
||||
Далее: [Kotlin](../Kotlin.md)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user