Аннотации
Аннотации — это метки, которые позволяют прикреплять метаданные к элементам кода. Инструменты и фреймворки обрабатывают эти метаданные во время компиляции и выполнения программы и выполняют разные действия на их основе.
С помощью аннотаций можно упростить и автоматизировать распространённые задачи, например генерировать шаблонный код, обеспечивать соблюдение стандартов кодирования или писать документацию.
Если вы хотите разрабатывать собственные обработчики аннотаций, используйте API Kotlin Symbol Processing (KSP).
Объявление
Аннотации — это специальный тип классов. Чтобы объявить аннотацию, используйте ключевое слово annotation перед
объявлением класса:
annotation class Fancy
Дополнительные атрибуты аннотаций могут быть определены путём аннотирования класса аннотации мета-аннотациями:
@Targetопределяет возможные виды элементов, которые могут быть помечены аннотацией (такие как классы, функции, свойства и выражения);@Retentionопределяет, будет ли аннотация храниться в скомпилированном классе и будет ли видна через рефлексию (по умолчанию оба утверждения верны);@Repeatableпозволяет использовать одну и ту же аннотацию на одном элементе несколько раз;@MustBeDocumentedопределяет то, что аннотация является частью публичного API и должна быть включена в сигнатуру класса или метода, попадающую в сгенерированную документацию.
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.VALUE_PARAMETER,
AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class Fancy
Использование
@Fancy class Foo {
@Fancy fun baz(@Fancy foo: Int): Int {
return (@Fancy 1)
}
}
Если вам нужно пометить аннотацией первичный конструктор класса, следует добавить ключевое слово constructor
при объявлении конструктора и вставить аннотацию перед ним:
class Foo @Inject constructor(dependency: MyDependency) { ... }
Вы также можете помечать аннотациями геттеры и сеттеры:
class Foo {
var x: MyDependency? = null
@Inject set
}
Конструкторы
Аннотации могут иметь конструкторы, принимающие параметры.
annotation class Special(val why: String)
@Special("пример") class Foo {}
Разрешены параметры следующих типов:
- типы, которые соответствуют примитивам Java (Int, Long и т.д.),
- строки,
- классы (
Foo::class), - перечисляемые типы,
- другие аннотации,
- массивы, содержащие значения приведённых выше типов.
Параметры аннотаций не могут иметь обнуляемые типы, потому что JVM не поддерживает хранение null в качестве значения
атрибута аннотации.
Если аннотация используется в качестве параметра к другой аннотации, её имя не нужно начинать со знака @.
annotation class ReplaceWith(val expression: String)
annotation class Deprecated(
val message: String,
val replaceWith: ReplaceWith = ReplaceWith(""))
@Deprecated("Эта функция устарела, вместо неё используйте ===", ReplaceWith("this === other"))
Если вам нужно указать класс как аргумент аннотации, используйте Kotlin-класс (KClass). Компилятор Kotlin автоматически сконвертирует его в Java-класс, так что код на Java сможет видеть аннотации и их аргументы.
import kotlin.reflect.KClass
annotation class Ann(val arg1: KClass<*>, val arg2: KClass<out Any>)
@Ann(String::class, Int::class) class MyClass
Создание экземпляра
В Java тип аннотации — это форма интерфейса, поэтому вы можете реализовать его и использовать экземпляр. В качестве альтернативы этому механизму Kotlin позволяет вызывать конструктор класса аннотации в произвольном коде и аналогичным образом использовать полученный экземпляр.
annotation class InfoMarker(val info: String)
fun processInfo(marker: InfoMarker): Unit = TODO()
fun main(args: Array<String>) {
if (args.isNotEmpty())
processInfo(getAnnotationReflective(args))
else
processInfo(InfoMarker("default"))
}
Узнайте больше о создании экземпляров классов аннотаций в этом KEEP.
Лямбды
Аннотации также можно использовать с лямбдами. Они будут применены к методу invoke(), в который генерируется тело
лямбды. Это полезно для фреймворков вроде Quasar, которые используют
аннотации для контроля многопоточности.
annotation class Suspendable
val f = @Suspendable { Fiber.sleep(10) }
Аннотации с указаниями
Когда вы помечаете аннотацией свойство или параметр первичного конструктора, из соответствующего Kotlin-элемента генерируются несколько Java-элементов, и поэтому в сгенерированном байт-коде для аннотации есть несколько возможных мест. Чтобы указать, как именно должна быть сгенерирована аннотация, используйте следующий синтаксис:
class Example(@field:Ann val foo, // аннотирует только Java-поле
@get:Ann val bar, // аннотирует только Java-геттер
@param:Ann val quux) // аннотирует только параметр Java-конструктора
Тот же синтаксис может быть использован для аннотации целого файла. Для этого поместите аннотацию с указанием file на
верхнем уровне файла: перед указанием пакета или перед всеми импортами, если файл находится в пакете по умолчанию.
@file:JvmName("Foo")
package org.jetbrains.demo
Если у вас есть несколько аннотаций с одним указанием, вы можете не повторять его: добавьте квадратные скобки после
указания и поместите все аннотации внутрь скобок (кроме мета-указания all).
class Example {
@set:[Inject VisibleForTesting]
var collaborator: Collaborator
}
Полный список поддерживаемых указаний:
filefieldproperty(такие аннотации не будут видны в Java)get(геттер свойства)set(сеттер свойства)all(экспериментальное мета-указание для свойств; назначение и использование описаны ниже)receiver(параметр-приёмник функции или свойства расширения)Чтобы пометить аннотацией параметр-приёмник функции расширения, используйте следующий синтаксис:
fun @receiver:Fancy String.myExtension() { ... }param(параметр конструктора)setparam(параметр сеттера)delegate(поле, которое хранит экземпляр делегата для делегированного свойства)
Значения по умолчанию, когда указания не заданы
Если вы не сделали указание, оно будет выбрано в соответствии с аннотацией @Target той аннотации, которую вы
используете. Если существует несколько применимых целей, будет выбрана первая подходящая цель из следующего списка:
parampropertyfield
Возьмём аннотацию
@Email из Jakarta Bean Validation:
@Target(value={METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER,TYPE_USE})
public @interface Email { }
Рассмотрим следующий пример с этой аннотацией:
data class User(val username: String,
// @Email эквивалентна @param:Email
@Email val email: String) {
// @Email эквивалентна @field:Email
@Email val secondaryEmail: String? = null
}
В Kotlin 2.2.0 появилось экспериментальное правило выбора значения по умолчанию, которое должно сделать распространение аннотаций на параметры, поля и свойства более предсказуемым.
С новым правилом, если существует несколько применимых целей, выбирается одна или несколько целей следующим образом:
- если применима цель параметра конструктора (
param), она используется; - если применима цель свойства (
property), она используется; - если применима цель поля (
field), аproperty— нет, используетсяfield.
Для того же примера:
data class User(val username: String,
// @Email теперь эквивалентна @param:Email @field:Email
@Email val email: String) {
// @Email по-прежнему эквивалентна @field:Email
@Email val secondaryEmail: String? = null
}
Если целей несколько и ни одна из param, property или field не применима, аннотация недопустима.
Чтобы включить новое правило выбора значения по умолчанию, используйте следующую строку в конфигурации Gradle:
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xannotation-default-target=param-property")
}
}
Если вам нужно использовать старое поведение, вы можете:
в конкретном случае явно указать нужную цель, например использовать
@param:Annotationвместо@Annotation;для всего проекта использовать этот флаг в файле сборки Gradle:
// build.gradle.kts kotlin { compilerOptions { freeCompilerArgs.add("-Xannotation-default-target=first-only") } }
Мета-указание all
Указание all упрощает применение одной и той же аннотации не только к параметру и свойству или полю, но и к
соответствующему геттеру и сеттеру.
В частности, аннотация с all распространяется, если это применимо:
- на параметр конструктора (
param), если свойство определено в первичном конструкторе; - на само свойство (
property); - на поле с резервным хранением (
field), если у свойства оно есть; - на геттер (
get); - на параметр сеттера (
setparam), если свойство определено какvar; - на Java-only цель
RECORD_COMPONENT, если у класса есть аннотация@JvmRecord.
Возьмём аннотацию
@Email из Jakarta Bean Validation,
которая определена следующим образом:
@Target(value={METHOD,FIELD,ANNOTATION_TYPE,CONSTRUCTOR,PARAMETER,TYPE_USE})
public @interface Email { }
В примере ниже аннотация @Email применяется ко всем релевантным целям:
data class User(
val username: String,
// Применяет @Email к param, field и get
@all:Email val email: String,
// Применяет @Email к param, field, get и setparam
@all:Email var name: String,
) {
// Применяет @Email к field и getter (не к param, потому что свойство не в конструкторе)
@all:Email val secondaryEmail: String? = null
}
Мета-указание all можно использовать с любым свойством как внутри первичного конструктора, так и вне его.
Ограничения
У указания all есть несколько ограничений:
оно не распространяет аннотацию на типы, возможные приёмники расширений, контекстные приёмники или параметры;
его нельзя использовать с несколькими аннотациями:
@all:[A B] // запрещено, используйте @all:A @all:B val x: Int = 5его нельзя использовать с делегированными свойствами.
Как включить
Чтобы включить мета-указание all в вашем проекте, используйте следующую опцию компилятора в командной строке:
-Xannotation-target-all
Или добавьте её в блок compilerOptions {} файла сборки Gradle:
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xannotation-target-all")
}
}
Java-аннотации
Java-аннотации на 100% совместимы в Kotlin:
import org.junit.Test
import org.junit.Assert.*
import org.junit.Rule
import org.junit.rules.*
class Tests {
// применение аннотации @Rule к геттеру свойства
@get:Rule val tempFolder = TemporaryFolder()
@Test fun simple() {
val f = tempFolder.newFile()
assertEquals(42, getTheAnswer())
}
}
Так как порядок параметров для Java-аннотаций не задан, вы не можете использовать обычный синтаксис вызова функции для передачи аргументов. Вместо этого вам нужно использовать именованные аргументы.
// Java
public @interface Ann {
int intValue();
String stringValue();
}
// Kotlin
@Ann(intValue = 1, stringValue = "abc") class C
Также, как и в Java, параметр value — особый случай; его значение может быть определено без явного указания имени.
// Java
public @interface AnnWithValue {
String value();
}
// Kotlin
@AnnWithValue("abc") class C
Массивы в качестве параметров аннотаций
Если аргумент value в Java является массивом, в Kotlin он становится vararg параметром.
// Java
public @interface AnnWithArrayValue {
String[] value();
}
// Kotlin
@AnnWithArrayValue("abc", "foo", "bar") class C
Для прочих аргументов, которые являются массивом, нужно использовать синтаксис литерала массива или arrayOf(...).
// Java
public @interface AnnWithArrayMethod {
String[] names();
}
@AnnWithArrayMethod(names = ["abc", "foo", "bar"])
class C
Доступ к свойствам экземпляра аннотации
Значения экземпляра аннотации становятся свойствами в Kotlin-коде.
// Java
public @interface Ann {
int value();
}
// Kotlin
fun foo(ann: Ann) {
val i = ann.value
}
Возможность не генерировать цели аннотаций JVM 1.8+
Если среди Kotlin-целей аннотации Kotlin есть TYPE, аннотация сопоставляется с
java.lang.annotation.ElementType.TYPE_USE в списке её Java-целей. Аналогично Kotlin-цель TYPE_PARAMETER
сопоставляется с Java-целью java.lang.annotation.ElementType.TYPE_PARAMETER. Это проблема для Android-клиентов с API
level ниже 26, где этих целей нет в API.
Чтобы не генерировать цели аннотаций TYPE_USE и TYPE_PARAMETER, используйте новый аргумент компилятора
-Xno-new-java-annotation-targets.
Повторяющиеся аннотации
Как и в Java, в Kotlin есть повторяющиеся
аннотации, которые можно применять к одному элементу кода несколько раз. Чтобы сделать вашу аннотацию повторяемой,
отметьте её объявление мета-аннотацией
@kotlin.annotation.Repeatable. Это
сделает её повторяемой как в Kotlin, так и в Java. Повторяющиеся аннотации Java также поддерживаются со стороны Kotlin.
Основным отличием от схемы, используемой в Java, является отсутствие содержащей аннотации, которую компилятор Kotlin
генерирует автоматически с предопределённым именем. Для аннотации в приведённом ниже примере будет сгенерирована
содержащая аннотация @Tag.Container.
@Repeatable
annotation class Tag(val name: String)
// Компилятор генерирует содержащую аннотацию @Tag.Container
Вы можете задать имя для содержащей аннотации, применив мета-аннотацию
@kotlin.jvm.JvmRepeatable и передав явно
объявленный класс содержащей аннотации в качестве аргумента.
@JvmRepeatable(Tags::class)
annotation class Tag(val name: String)
annotation class Tags(val value: Array<Tag>)
Чтобы извлечь повторяющиеся аннотации Kotlin или Java с помощью рефлексии, используйте функцию
KAnnotatedElement.findAnnotations().
Узнайте больше о повторяющихся аннотациях Kotlin в этом KEEP.