从Java调用Kotlin代码

在此代码实验室中,您将学习如何编写或改编Kotlin代码,以使其可以从Java代码中无缝调用。

您将学到什么

  • 如何利用@JvmField@JvmStatic和其他注释。
  • 从Java代码访问某些Kotlin语言功能的限制。

你必须已经知道的

该代码实验室是为程序员编写的,并假定您具有Java和Kotlin的基本知识。

该代码实验室模拟了用Java编程语言编写的较大项目的迁移部分,以合并新的Kotlin代码。

为简化起见,我们将使用一个名为UseCase.java .java文件来表示现有的代码库。

我们可以想象我们只是用Kotlin编写的新版本替换了最初用Java编写的某些功能,而我们需要完成对它们的集成。

导入项目

可以从以下GitHub项目中克隆该项目的代码: GitHub

或者,您可以从以下位置的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()但有一个更好的办法。

我们可以通过使用@JvmStatic注释存储库的公共属性和方法,使Kotlin生成静态方法和属性:

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)
   }
}

使用您的IDE将@JvmStatic批注添加到您的代码中。

如果我们切换回UseCase.java ,在属性和方法Repository不再导致错误,除了Repository.BACKUP_PATH 。我们待会儿再讲。

现在,让我们修复registerGuest()方法中的下一个错误。

让我们考虑以下情况:我们有一个StringUtils类,其中包含几个用于字符串操作的静态函数。当我们将其转换为Kotlin时,我们将方法转换为扩展函数。 Java没有扩展功能,因此Kotlin将这些方法编译为静态函数。

不幸的是,如果我们查看UseCase.java中的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

我们可以使用@file:JvmName批注告诉Kotlin对包级方法使用不同的名称。让我们使用此批注将类命名为StringUtils

@file:JvmName("StringUtils")

package com.google.example.javafriendlykotlin

现在,如果我们回顾UseCase.java ,可以看到StringUtils.nameToLogin()的错误已解决。

不幸的是,此错误被替换为有关传递给User的构造函数的参数的新错误。让我们继续下一步,并修复UseCase.registerGuest()最后一个错误。

Kotlin支持参数的默认值。我们可以通过查看Repository.ktinit块来了解它们的用法。

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);
}

我们可以只用两个参数idusername来构造一个User

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

我们还可以通过为displayName包括第三个参数来构造一个User ,同时仍然使用groups的默认值:

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

但是无法跳过displayName并仅为groups提供一个值而无需编写其他代码是不可能的:

e33040916c6c32e4.png

因此,让我们删除该行或在其前面加上“ //”以将其注释掉。

在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;
}

这是一个有趣的错误!如果在类User上使用IDE的自动完成功能,您会注意到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

对于使用默认的,隐式定义的getter的属性,alternate方法特别有用。例如:

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

这允许更改吸气剂的名称,而不必显式定义吸气剂。

尽管有这种区别,您可以使用对自己感觉更好的一种。两者都将导致Kotlin创建名称为hasSystemAccess()的吸气剂。

如果我们切换回UseCase.java ,则可以验证getSystemUsers()现在没有错误!

下一个错误是在formatUser() ,但是如果您想了解有关Kotlin getter命名约定的更多信息,请继续阅读此处,然后继续下一步。

Getter和Setter命名

当我们编写Kotlin时,很容易忘记编写如下代码:

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

实际上是在调用一个函数来获取displayName的值。我们可以通过以下方法来验证这一点:转到菜单中的工具> Kotlin>显示Kotlin字节码,然后单击反编译按钮:

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

当我们想从Java访问它们时,我们需要显式地写出getter的名称。

在大多数情况下,如我们在User.getHasSystemAccess()User.getDisplayName()看到的那样,Kotlin属性的getter的Java名称就是get +属性名。一个例外是名称以“ is”开头的属性。在这种情况下,getter的Java名称是Kotlin属性的名称。

例如, User的属性,例如:

val isAdmin get() = //...

将通过以下方式从Java访问:

boolean userIsAnAdmin = user.isAdmin();

通过使用@JvmName注释,Kotlin会为要注释的项目生成具有指定名称而不是默认名称的字节码。

这对于s​​etter的工作原理相同,其生成的名称始终为set +属性名。例如,采用以下课程:

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

假设我们想将setter名称从setRed()更改为updateRed() ,而将getter保留setRed() 。我们可以使用@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将跳过生成getter(和var属性的setter)方法的方法,并且可以直接访问后备字段。

由于User对象是不可变的,因此我们希望将它们的每个属性都显示为字段,因此我们将使用@JvmField对其进行@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或const

这样,在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类型是基元,例如intfloatString 。在这种情况下,因为BACKUP_PATH是字符串,所以我们可以通过使用const val而不是使用@JvmField注释的val来获得更好的性能,同时保留将值作为字段访问的能力。

现在在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.saveAsRepository.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函数引发异常,则需要对Kotlin函数签名使用@Throws批注切换到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中的所有编译器错误UseCase.java修复!万岁!

您可能想知道现在从Kotlin调用saveAs()时是否必须使用trycatch块。

不!记住,Kotlin没有检查异常,将@Throws添加到方法不会改变这一点:

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

在可以处理异常时捕获异常仍然有用,但是Kotlin不会强迫您处理异常。

在此代码实验室中,我们介绍了如何编写Kotlin代码的基础知识,该代码还支持编写惯用的Java代码。

我们讨论了如何使用注释来更改Kotlin生成JVM字节码的方式,例如:

  • @JvmStatic生成静态成员和方法。
  • @JvmOverloads可以为具有默认值的函数生成重载方法。
  • @JvmName更改获取器和设置器的名称。
  • @JvmField将属性直接公开为字段,而不是通过getter和setter公开。
  • @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
}