Аннотации

Аннотации — это метки, которые позволяют прикреплять метаданные к элементам кода. Инструменты и фреймворки обрабатывают эти метаданные во время компиляции и выполнения программы и выполняют разные действия на их основе.

С помощью аннотаций можно упростить и автоматизировать распространённые задачи, например генерировать шаблонный код, обеспечивать соблюдение стандартов кодирования или писать документацию.

Если вы хотите разрабатывать собственные обработчики аннотаций, используйте 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
}

Полный список поддерживаемых указаний:

  • file

  • field

  • property (такие аннотации не будут видны в Java)

  • get (геттер свойства)

  • set (сеттер свойства)

  • all (экспериментальное мета-указание для свойств; назначение и использование описаны ниже)

  • receiver (параметр-приёмник функции или свойства расширения)

    Чтобы пометить аннотацией параметр-приёмник функции расширения, используйте следующий синтаксис:

    fun @receiver:Fancy String.myExtension() { ... }
    
  • param (параметр конструктора)

  • setparam (параметр сеттера)

  • delegate (поле, которое хранит экземпляр делегата для делегированного свойства)

Значения по умолчанию, когда указания не заданы

Если вы не сделали указание, оно будет выбрано в соответствии с аннотацией @Target той аннотации, которую вы используете. Если существует несколько применимых целей, будет выбрана первая подходящая цель из следующего списка:

  • param
  • property
  • field

Возьмём аннотацию @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.