Pull to refresh

Искусство аннотирования: пишем Java-friendly код на Kotlin

Reading time 6 min
Views 6.8K
Original author: Avantica
Привет, Хабр!

Сегодня мы затронем важнейшую тему: интероперабельность Java и Kotlin. Авторы предлагаемой публикации разумно предполагают, что переписать на Kotlin базу кода, сделанную на Java, маловозможно. Поэтому правильнее обеспечить взаимодействие кода на Java и Kotlin. Читайте, как это можно сделать при помощи аннотаций.

Думаю, вы открыли эту статью по следующим причинам:

  1. Наконец-то решили попробовать Kotlin.
  2. Он вам понравился, что, впрочем, неудивительно.
  3. Решили использовать Kotlin повсюду
  4. Столкнулись с суровой реальностью: от Java совсем отказаться не получается, как минимум, малой кровью.

Почему?

Если Kotlin такой классный, почему бы не использовать его везде и всегда? Вот, навскидку, пара сценариев, в которых это невозможно:

  1. Когда вы пытаетесь медленно перенести всю вашу базу кода на Kotlin, вы заметите, что попадаются такие файлы, к которым попросту страшно применить команду Convert Java to Kotlin file. Если у вас есть время на рефакторинг – займитесь им! Однако, в реальном проекте время на рефакторинг найдется не всегда.
  2. Ваш код будут использовать программисты, работающие как с Java, так и с Kotlin. Вы не можете (или не должны) вынуждать их всех использовать конкретный язык, особенно если поддержка обоих языков не потребует от вас больших усилий (естественно, я говорю об аннотациях).

Здесь мы рассмотрим несколько аннотаций, обеспечивающих интероперабельность между Java и Kotlin!

Аннотации Java

JvmField

  • Что она делает? Приказывает компилятору Kotlin не генерировать геттеры и сеттеры для данного свойства и предоставить его как поле.
  • Наиболее распространенный практический случай: предоставить поля объекта-компаньона.

Как это работает?

Допустим, вы определяете поле внутри object / companion object в Kotlin:

object Constants {
val PERMISSIONS = listOf("Internet", "Location")
}

Если вы попытаетесь вызвать эту функцию из Java, то придется написать:

Utils.INSTANCE.getPERMISSIONS()

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

object Constants {
@JvmField
val PERMISSIONS = listOf("Internet", "Location")
}

Теперь наш код на Java будет выглядеть так:

Utils.PERMISSIONS;

Того же самого можно достичь и при помощи модификатора, однако, такой модификатор работает лишь с примитивными типами или строками.

//Kotin
object Constants {
const val KEY = "test"
}


//Java
String key = Constant.KEY;

Когда эту аннотацию нельзя использовать?

Свойства const, помеченные как and-функции, нельзя аннотировать @JvmField

JvmStatic

  • Что она делает? Если она используется с функцией, то указывает, что из этого элемента должен быть сгенерирован дополнительный статический метод. Если она используется со свойством, то будут генерироваться дополнительные статические методы-геттеры и методы-сеттеры.
  • Наиболее распространенный практический случай: предоставление членов (функций, свойств) из объекта-компаньона.

Как это работает?

Допустим, вы определяете функцию в object в Kotlin:

object Utils {
fun doSomething(){ ... }
}

Если попытаетесь вызвать эту функцию из Java, то придется написать:

Utils.INSTANCE.doSomething()

Нам приходится обращаться к объекту INSTANCE всякий раз, когда мы хотим вызвать эту функцию. Чтобы сделать код чище, давайте лучше воспользуемся аннотацией @JvmStatic.

object Utils {
@JvmStatic
fun doSomething(){ ... }
}

Теперь, вызывая эту функцию из Java, мы должны будем написать всего лишь:

Utils.doSomething();

Так гораздо лучше, не правда ли? Ситуация такова, как если бы функция была исходно написана на Java как статический метод.

Также аннотации можно применять и к полям:

object Utils {
@JvmStatic
var values = listOf("Test 1", "Test 2")
}

Вызывая этот код из Java, можно написать:

Utils.getValues();

Обратите внимание: JvmField предоставляет член как поле, но с JvmStatic мы предоставляем функцию get.

А поскольку поле это var, также генерируется метод set:

Utils.setValues(...);

Если у нас внутри нашего объекта есть константа, то ее мы тоже можем объявить как статическую:

object Utils {
@JvmStatic
val KEY = "test"
}

Однако, в данном случае использование аннотации – не лучшая идея, поскольку вызов на инвокацию выглядел бы так:

public void foo(){
String key = Utils.getKEY();
}

В таком случае используйте модификатор const или JvmField, как было объяснено выше.

Когда ее нельзя использовать?

Член нельзя аннотировать JvmStatic, когда он сопровождается модификатором open, override или const.

В такой ситуации код не скомпилируется:



JvmOverloads

  • Что она делает? Приказывает компилятору Kotlin сгенерировать перегрузки для данной функции, которая заменяет значения параметров, заданные по умолчанию.
  • Что такое “перегрузка”? В Kotlin у вашей функции могут быть параметры по умолчанию, благодаря чему можно вызывать одну и ту же функцию различными способами. Чтобы достичь того же в Java, пришлось бы вручную определять каждую отдельную вариацию этой функции. Каждая из таких автоматически сгенерированных вариаций называется «перегрузкой». Наиболеее распространенный вариант использования: перегрузка конструкторов классов. Да, такой прием работает с любой функцией, у которой есть параметры по умолчанию.

Как это работает?

Если у вас есть класс с конструктором (или любой другой функцией) с параметрами, заданными по умолчанию…

class User constructor (
val name: String = "Test",
val lastName: String = "Testy", val age: Int = 0
)
 

… то вы сможете вызывать такую функцию из Kotlin различными способами:

val user1 = User()
val user2 = User(name = "Bruno")
val user3 = User(name = "Bruno", lastName = "Aybar")
val user4 = User(name = "Bruno", lastName = "Aybar", age = 21)
val user5 = User(lastName = "Aybar")
val user6 = User(lastName = "Aybar", age = 21) val user7 = User(age = 21)
val user8 = User(age = 21, name = "Bruno")
...

Однако, если вы попытаетесь вызвать конструктор из Java, у вас будет всего два варианта: 1) передать все параметры или 2) только в случае, когда у ВСЕХ ваших параметров есть значения по умолчанию, можно не передавать вообще никаких параметров.

Если мы хотим создать перегрузки, то можем воспользоваться аннотацией JvmOverloads:

class User @JvmOverloads constructor ( val name: String = "Test",
val lastName: String = "Testy", val age: Int = 0
)

Теперь при использовании Java у нас появляется множество возможностей:



Однако, в Kotlin в данном случае вариантов не так много. Например, мы не сможем передать только фамилию или только возраст.

Аннотация JvmOverloads сгенерирует лишь столько перегрузок, сколько есть у функции параметров, заданных по умолчанию.

  • Если у вас есть функция, то ее можно пометить как JvmOverload. Можно даже скомбинировать ее с другими аннотациями, например, с JvmStatic.
  • Когда ее не следует использовать? Эта аннотация бесполезна, если у функции нет параметров, заданных по умолчанию.

file:JvmName

  • Что она делает? Указывает имя для класса или метода Java, генерируемого из данного элемента.
  • Наиболее распространенный случай: дать более красивое имя файлу Kotlin. Однако, эта аннотация применима не только с файлами, но и с функциями, а также с методами для доступа к свойствам (геттерами и сеттерами).

Как она работает?

В Kotlin, где функции являются привилегированными элементами, можно писать функции, существующие вне класса. Например, если вы создаете новый файл Kotlin и пишете следующий код, то он скомпилируется без проблем:

//file name: Utils.kt

fun doSomething() { ... }
 

Можно вызвать этот код из Java:

UtilsKt.doSomething();

Обратите внимание: хотя файл и называется Utils, при вызове используется имя UtilsKt, а это не идеально. Чтобы это исправить, давайте добавим сверху файла аннотацию JvmName.

// имя файла: Utils.kt
@file:JvmName("Utils")

fun doSomething() { ... }

Обратите внимание, как используется префикс file:. Вероятно, вы уже догадались: он указывает, что используемая нами аннотация применяется на уровне файлов. Если вызвать следующий код из Java:

Utils.doSomething();

То также можно аннотировать функции:

// имя файла: Utils.kt @file:JvmName("Utils")

@JvmName("doSomethingElse")
fun doSomething() { ... }

При вызове этого кода из Kotlin, мы все равно будем пользоваться оригинальным именем (doSomething), но в Java мы используем имя, указанное в аннотации:

//Java Utils.doSomethingElse();


//Kotlin Utils.doSomething()
 

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

Здесь можно работать и с методами для доступа к свойствам:

class User {
val likesKotlin: Boolean = true
@JvmName("likesKotlin") get() = field
}

Смотрите, как будет выглядеть этот вызов в Java с аннотацией и без нее:

// Без аннотации
new User().getLikesKotlin()

// С аннотацией
new User().likesKotlin()

Того же самого можно достичь при помощи префикса get.

class User {
@get:JvmName("likesKotlin")
val likesKotlin = true
}
 

  • В каких случаях можно использовать такую возможность? С файлами, функциями, методами для доступа к свойствам. Однако, обязательно ставьте нужные префиксы в случае необходимости.
  • Когда ею не следует пользоваться? Если произвольно задать функции альтернативное имя, то можно устроить большую путаницу. Пользуйтесь этой аннотацией осторожно, а если применяете – то применяйте согласованно.

Надеюсь, вам пригодился этот обзор аннотаций, помогающий писать на Kotlin код, удобный для использования с Java.
Tags:
Hubs:
+12
Comments 2
Comments Comments 2

Articles

Information

Website
piter.com
Registered
Founded
Employees
201–500 employees
Location
Россия