Вызов кода Kotlin из Java

В этой лаборатории кода вы узнаете, как написать или адаптировать свой код Kotlin, чтобы сделать его более удобным для вызова из кода Java.

Что вы узнаете

  • Как использовать @JvmField , @JvmStatic и другие аннотации.
  • Ограничения доступа к определенным функциям языка Kotlin из кода Java.

Что вы уже должны знать

Эта лабораторная работа написана для программистов и предполагает базовые знания Java и Kotlin.

Эта лаборатория кода имитирует миграцию части более крупного проекта, написанного на языке программирования Java, для включения нового кода Kotlin.

Для упрощения у нас будет один файл .java с именем UseCase.java , который будет представлять существующую кодовую базу.

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

Импортировать проект

Код проекта можно клонировать из проекта GitHub здесь: GitHub

Кроме того, вы можете загрузить и распаковать проект из zip-архива, который можно найти здесь:

Скачать ZIP

Если вы используете IntelliJ IDEA, выберите «Импортировать проект».

Если вы используете Android Studio, выберите «Импортировать проект (Gradle, Eclipse ADT и т. д.)».

Давайте откроем UseCase.java и начнем работать с ошибками, которые мы видим.

Первая функция с проблемой — registerGuest :

public static User registerGuest(String name) {
   User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);
   Repository.addUser(guest);
   return guest;
}

Ошибки для Repository.getNextGuestId() и Repository.addUser(...) одинаковы: «Нестатический доступ невозможен из статического контекста».

Теперь давайте взглянем на один из файлов Kotlin. Откройте файл Repository.kt .

Мы видим, что наш репозиторий — это синглтон, объявленный с помощью ключевого слова object. Проблема в том, что Kotlin генерирует статический экземпляр внутри нашего класса, а не предоставляет его как статические свойства и методы.

Например, на Repository.getNextGuestId() можно ссылаться с помощью Repository.INSTANCE.getNextGuestId() , но есть способ получше.

Мы можем заставить Kotlin генерировать статические методы и свойства, аннотируя общедоступные свойства и методы репозитория с помощью @JvmStatic :

object Repository {
   val BACKUP_PATH = "/backup/user.repo"

   private val _users = mutableListOf<User>()
   private var _nextGuestId = 1000

   @JvmStatic
   val users: List<User>
       get() = _users

   @JvmStatic
   val nextGuestId
       get() = _nextGuestId++

   init {
       _users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
       _users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
       _users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
       _users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
   }

   @JvmStatic
   fun saveAs(path: String?):Boolean {
       val backupPath = path ?: return false

       val outputFile = File(backupPath)
       if (!outputFile.canWrite()) {
           throw FileNotFoundException("Could not write to file: $backupPath")
       }
       // Write data...
       return true
   }

   @JvmStatic
   fun addUser(user: User) {
       // Ensure the user isn't already in the collection.
       val existingUser = users.find { user.id == it.id }
       existingUser?.let { _users.remove(it) }
       // Add the user.
       _users.add(user)
   }
}

Добавьте аннотацию @JvmStatic к вашему коду, используя вашу IDE.

Если мы вернемся к UseCase.java , свойства и методы в Repository больше не будут вызывать ошибок, за исключением Repository.BACKUP_PATH . Мы вернемся к этому позже.

А пока исправим следующую ошибку в методе registerGuest() .

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

К сожалению, если мы посмотрим на метод registerGuest() внутри UseCase.java , мы увидим, что что-то не так:

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

Причина в том, что Kotlin помещает эти функции «верхнего уровня» или уровня пакета внутри класса, имя которого основано на имени файла. В этом случае, поскольку файл называется StringUtils.kt, соответствующий класс называется StringUtilsKt .

Мы могли бы изменить все наши ссылки StringUtils на StringUtilsKt и исправить эту ошибку, но это не идеально, потому что:

  • В нашем коде может быть много мест, которые необходимо обновить.
  • Само название неудобное.

Поэтому вместо того, чтобы реорганизовать наш код Java, давайте обновим наш код Kotlin, чтобы использовать другое имя для этих методов.

Откройте StringUtils.Kt и найдите следующее объявление пакета:

package com.google.example.javafriendlykotlin

Мы можем указать Kotlin использовать другое имя для методов уровня пакета, используя аннотацию @file:JvmName . Давайте используем эту аннотацию, чтобы назвать класс StringUtils .

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

Теперь, если мы вернемся к UseCase.java , мы увидим, что ошибка для StringUtils.nameToLogin() устранена.

К сожалению, эта ошибка была заменена новой ошибкой о передаче параметров в конструктор для User . Давайте перейдем к следующему шагу и исправим последнюю ошибку в UseCase.registerGuest() .

Kotlin поддерживает значения по умолчанию для параметров . Мы можем увидеть, как они используются, заглянув внутрь блока init Repository.kt .

Repository.kt:

_users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
_users.add(User(103, "warlow", groups = listOf("staff", "inactive")))

Мы видим, что для пользователя «warlow» мы можем пропустить ввод значения для displayName , потому что для него указано значение по умолчанию в User.kt

User.kt:

data class User(
   val id: Int,
   val username: String,
   val displayName: String = username.toTitleCase(),
   val groups: List<String> = listOf("guest")
)

К сожалению, это не работает при вызове метода из Java.

UseCase.java:

User guest = new User(Repository.getNextGuestId(), StringUtils.nameToLogin(name), name);

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

Во-первых, мы должны сделать небольшое обновление User.kt

Поскольку класс User имеет только один основной конструктор , а конструктор не содержит никаких аннотаций, ключевое слово constructor было опущено. Однако теперь, когда мы хотим его аннотировать, ключевое слово constructor должно быть включено:

data class User constructor(
    val id: Int,
    val username: String,
    val displayName: String = username.toTitleCase(),
    val groups: List<String> = listOf("guest")
)

Имея ключевое слово constructor , мы можем добавить аннотацию @JvmOverloads :

data class User @JvmOverloads constructor(
    val id: Int,
    val username: String,
    val displayName: String = username.toTitleCase(),
    val groups: List<String> = listOf("guest")
)

Если мы вернемся к UseCase.java , мы увидим, что в функции registerGuest больше нет ошибок!

Наш следующий шаг — исправить неработающий вызов user.hasSystemAccess() в UseCase.getSystemUsers() . Перейдите к следующему шагу для этого или продолжайте читать, чтобы углубиться в то, что @JvmOverloads сделал для исправления ошибки.

@JvmOverloads

Чтобы лучше понять, что делает @JvmOverloads , давайте создадим тестовый метод в UseCase.java :

private void testJvmOverloads() {
   User syrinx = new User(1001, "syrinx");
   User ione = new User(1002, "ione", "Ione Saldana");

   List<String> groups = new ArrayList<>();
   groups.add("staff");
   User beaulieu = new User(1002, "beaulieu", groups);
}

Мы можем создать User всего с двумя параметрами, id и username :

User syrinx = new User(1001, "syrinx");

Мы также можем создать User , включив третий параметр для displayName , но при этом используя значение по умолчанию для groups :

User ione = new User(1002, "ione", "Ione Saldana");

Но невозможно пропустить displayName и просто указать значение для groups без написания дополнительного кода:

Итак, давайте удалим эту строку или начнем с «//», чтобы закомментировать ее.

В Kotlin, если мы хотим объединить параметры по умолчанию и не по умолчанию, нам нужно использовать именованные параметры.

// This doesn't work...
User(104, "warlow", listOf("staff", "inactive"))
// But using named parameters, it does...
User(104, "warlow", groups = listOf("staff", "inactive"))

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

Давайте вернемся к UseCase.java и решим нашу следующую проблему: вызов user.hasSystemAccess() в методе UseCase.getSystemUsers() :

public static List<User> getSystemUsers() {
   ArrayList<User> systemUsers = new ArrayList<>();
   for (User user : Repository.getUsers()) {
       if (user.hasSystemAccess()) {     // Now has an error!
           systemUsers.add(user);
       }
   }
   return systemUsers;
}

Это интересная ошибка! Если вы используете функцию автозаполнения вашей IDE для класса User , вы заметите, что hasSystemAccess() была переименована в getHasSystemAccess() .

Чтобы решить эту проблему, мы хотели бы, чтобы Kotlin сгенерировал другое имя для свойства val hasSystemAccess . Для этого мы можем использовать аннотацию @JvmName . Давайте вернемся к User.kt и посмотрим, где мы должны его применить.

Есть два способа применить аннотацию. Первый — применить его непосредственно к методу get() , например так:

val hasSystemAccess
   @JvmName("hasSystemAccess")
   get() = "sys" in groups

Это сигнализирует Kotlin изменить подпись явно определенного геттера на указанное имя.

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

@get:JvmName("hasSystemAccess")
val hasSystemAccess
   get() = "sys" in groups

Альтернативный метод особенно полезен для свойств, которые используют неявно определенный геттер по умолчанию. Например:

@get:JvmName("isActive")
val active: Boolean

Это позволяет изменить имя получателя без явного определения получателя.

Несмотря на это различие, вы можете использовать то, что вам больше нравится. Оба заставят Kotlin создать геттер с именем hasSystemAccess() .

Если мы вернемся к UseCase.java , мы сможем убедиться, что getSystemUsers() теперь не содержит ошибок!

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

Именование геттера и сеттера

Когда мы пишем Kotlin, легко забыть об этом при написании кода, такого как:

val myString = "Logged in as ${user.displayName}")

Фактически вызывает функцию для получения значения displayName . Мы можем убедиться в этом, перейдя в меню « Инструменты» > «Kotlin» > «Показать байт-код Kotlin» , а затем нажав кнопку « Декомпилировать »:

String myString = "Logged in as " + user.getDisplayName();

Когда мы хотим получить к ним доступ из Java, нам нужно явно указать имя получателя.

В большинстве случаев имя геттера Java для свойств Kotlin — это просто get + имя свойства, как мы видели с User.getHasSystemAccess() и User.getDisplayName() . Единственным исключением являются свойства, имена которых начинаются с «is». В этом случае имя Java для геттера — это имя свойства Kotlin.

Например, свойство User , такое как:

val isAdmin get() = //...

Доступ к Java будет осуществляться с помощью:

boolean userIsAnAdmin = user.isAdmin();

Используя аннотацию @JvmName , Kotlin генерирует байт-код с указанным именем, а не с именем по умолчанию, для аннотируемого элемента.

То же самое работает и для сеттеров, чьи сгенерированные имена всегда представляют собой set + имя свойства. Например, возьмем следующий класс:

class Color {
   var red = 0f
   var green = 0f
   var blue = 0f
}

Давайте представим, что мы хотели бы изменить имя сеттера с setRed() на updateRed() , оставив геттеры в покое. Для этого мы могли бы использовать @set:JvmName :

class Color {
   @set:JvmName("updateRed")
   var red = 0f
   @set:JvmName("updateGreen")
   var green = 0f
   @set:JvmName("updateBlue")
   var blue = 0f
}

Тогда из Java мы сможем написать:

color.updateRed(0.8f);

UseCase.formatUser() использует прямой доступ к полю для получения значений свойств объекта User .

В Kotlin свойства обычно доступны через геттеры и сеттеры. Сюда входят свойства val .

Это поведение можно изменить с помощью аннотации @JvmField . Когда это применяется к свойству в классе, Kotlin пропустит создание методов получения (и установки для var свойств), а к резервному полю можно получить прямой доступ.

Поскольку объекты User неизменяемы, мы хотели бы представить каждое из их свойств в виде полей, и поэтому мы аннотируем каждое из них с помощью @JvmField :

data class User @JvmOverloads constructor(
   @JvmField val id: Int,
   @JvmField val username: String,
   @JvmField val displayName: String = username.toTitleCase(),
   @JvmField val groups: List<String> = listOf("guest")
) {
   @get:JvmName("hasSystemAccess")
   val hasSystemAccess
       get() = "sys" in groups
}

Если мы вернемся к UseCase.formatUser() , то увидим, что ошибки были исправлены!

@JvmField или константа

При этом в файле UseCase.java есть еще одна похожая ошибка:

Repository.saveAs(Repository.BACKUP_PATH);

Если мы воспользуемся здесь автозаполнением, мы увидим, что есть Repository.getBACKUP_PATH() , и поэтому может возникнуть соблазн изменить аннотацию в BACKUP_PATH с @JvmStatic на @JvmField .

Давайте попробуем это. Вернитесь к Repository.kt и обновите аннотацию:

object Repository {
   @JvmField
   val BACKUP_PATH = "/backup/user.repo"

Если мы сейчас посмотрим на UseCase.java , то увидим, что ошибка исчезла, но также есть примечание к BACKUP_PATH :

В Kotlin единственными типами, которые могут быть const , являются примитивы, такие как int , float и String . В этом случае, поскольку BACKUP_PATH является строкой, мы можем повысить производительность, используя const val , а не val с аннотацией @JvmField , сохраняя при этом возможность доступа к значению как к полю.

Давайте изменим это сейчас в Repository.kt:

object Repository {
   const val BACKUP_PATH = "/backup/user.repo"

Если мы вернемся к UseCase.java , то увидим, что осталась только одна ошибка.

Последняя ошибка говорит об Exception: 'java.io.IOException' is never thrown in the corresponding try block.

Однако если мы посмотрим на код Repository.saveAs в Repository.kt , мы увидим, что он генерирует исключение. В чем дело?

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

Поскольку проверенные исключения проверяются во время компиляции, вы объявляете их в сигнатуре метода:

public void openFile(File file) throws FileNotFoundException {
   // ...
}

Kotlin, с другой стороны, не имеет проверенных исключений, и это вызывает здесь проблему.

Решение состоит в том, чтобы попросить Kotlin добавить потенциально выбрасываемое IOException в сигнатуру Repository.saveAs() , чтобы байт-код JVM включал его как проверенное исключение.

Мы делаем это с помощью аннотации Kotlin @Throws , которая помогает с совместимостью Java/Kotlin. В Kotlin исключения ведут себя так же, как в Java, но, в отличие от Java, в Kotlin есть только непроверенные исключения. Поэтому, если вы хотите сообщить своему Java-коду, что функция Kotlin выдает исключение, вам нужно использовать аннотацию @Throws к сигнатуре функции Kotlin. Переключитесь на Repository.kt file и обновите saveAs() , чтобы включить новую аннотацию:

@JvmStatic
@Throws(IOException::class)
fun saveAs(path: String?) {
   val outputFile = File(path)
   if (!outputFile.canWrite()) {
       throw FileNotFoundException("Could not write to file: $path")
   }
   // Write data...
}

Имея аннотацию @Throws , мы видим, что все ошибки компилятора в UseCase.java исправлены! Ура!

Вы можете задаться вопросом, придется ли вам теперь использовать блоки try и catch при вызове saveAs() из Kotlin.

Неа! Помните, что в Kotlin нет проверенных исключений, и добавление @Throws к методу этого не меняет:

fun saveFromKotlin(path: String) {
   Repository.saveAs(path)
}

По-прежнему полезно перехватывать исключения, когда их можно обработать, но Kotlin не заставляет вас их обрабатывать.

В этой лаборатории кода мы рассмотрели основы написания кода Kotlin, который также поддерживает написание идиоматического кода Java.

Мы говорили о том, как мы можем использовать аннотации, чтобы изменить способ, которым Kotlin генерирует свой байт-код JVM, например:

  • @JvmStatic для создания статических членов и методов.
  • @JvmOverloads для создания перегруженных методов для функций со значениями по умолчанию.
  • @JvmName для изменения имени геттеров и сеттеров.
  • @JvmField для предоставления свойства непосредственно в виде поля, а не через геттеры и сеттеры.
  • @Throws объявление проверенных исключений.

Окончательное содержимое наших файлов:

User.kt

data class User @JvmOverloads constructor(
   @JvmField val id: Int,
   @JvmField val username: String,
   @JvmField val displayName: String = username.toTitleCase(),
   @JvmField val groups: List<String> = listOf("guest")
) {
   val hasSystemAccess
       @JvmName("hasSystemAccess")
       get() = "sys" in groups
}

Репозиторий.kt

object Repository {
   const val BACKUP_PATH = "/backup/user.repo"

   private val _users = mutableListOf<User>()
   private var _nextGuestId = 1000

   @JvmStatic
   val users: List<User>
       get() = _users

   @JvmStatic
   val nextGuestId
       get() = _nextGuestId++

   init {
       _users.add(User(100, "josh", "Joshua Calvert", listOf("admin", "staff", "sys")))
       _users.add(User(101, "dahybi", "Dahybi Yadev", listOf("staff", "nodes")))
       _users.add(User(102, "sarha", "Sarha Mitcham", listOf("admin", "staff", "sys")))
       _users.add(User(103, "warlow", groups = listOf("staff", "inactive")))
   }

   @JvmStatic
   @Throws(IOException::class)
   fun saveAs(path: String?):Boolean {
       val backupPath = path ?: return false

       val outputFile = File(backupPath)
       if (!outputFile.canWrite()) {
           throw FileNotFoundException("Could not write to file: $backupPath")
       }
       // Write data...
       return true
   }

   @JvmStatic
   fun addUser(user: User) {
       // Ensure the user isn't already in the collection.
       val existingUser = users.find { user.id == it.id }
       existingUser?.let { _users.remove(it) }
       // Add the user.
       _users.add(user)
   }
}

StringUtils.kt

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

fun String.toTitleCase(): String {
   if (isNullOrBlank()) {
       return this
   }

   return split(" ").map { word ->
       word.foldIndexed("") { index, working, char ->
           val nextChar = if (index == 0) char.toUpperCase() else char.toLowerCase()
           "$working$nextChar"
       }
   }.reduceIndexed { index, working, word ->
       if (index > 0) "$working $word" else word
   }
}

fun String.nameToLogin(): String {
   if (isNullOrBlank()) {
       return this
   }
   var working = ""
   toCharArray().forEach { char ->
       if (char.isLetterOrDigit()) {
           working += char.toLowerCase()
       } else if (char.isWhitespace() and !working.endsWith(".")) {
           working += "."
       }
   }
   return working
}