在此 Codelab 中,您将学习如何编写或修改 Kotlin 代码,从而更加顺畅地在 Java 代码中调用 Kotlin 代码。
学习内容
- 如何使用
@JvmField
、@JvmStatic
和其他注解。 - 在通过 Java 代码访问某些 Kotlin 语言功能方面存在的限制。
学前须知
此 Codelab 是专为程序员编写的,因此我们假定您具备 Java 和 Kotlin 方面的基础知识。
此 Codelab 会模拟对一个使用 Java 编程语言编写的大型项目的部分内容进行迁移,以便与新的 Kotlin 代码整合的过程。
为简单起见,我们将使用一个名为 UseCase.java
的 .java
文件,该文件将代表现有代码库。
我们将假定,我们刚刚将最初使用 Java 编写的部分功能替换为使用 Kotlin 编写的新版本,并且我们需要完成相关的集成。
导入项目
您可以访问以下网址以从 GitHub 项目复制项目代码:GitHub
或者,您也可以访问以下网址以下载 ZIP 归档文件并从中解压项目:
如果您使用的是 IntelliJ IDEA,请选择“Import Project”。
如果您使用的是 Android Studio,请选择“Import project (Gradle, Eclipse ADT, etc.)”。
请打开 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(...)
的错误是相同的:“Non-static cannot be accessed from a static context”。
现在,我们要看一看其中一个 Kotlin 文件。打开文件 Repository.kt
。
可以看到,我们的代码库是通过使用对象关键字进行声明的单例。问题在于,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()
方法,就会发现情况不太对劲:
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.kt
的 init
代码块的内容来了解默认值使用情况。
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 编程语言不支持默认值。为解决这个问题,我们要借助 @JvmOverloads 注解让 Kotlin 针对构造函数生成重载。
首先,我们必须对 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
函数中已经没有任何错误了!
下一步要修复 UseCase.getSystemUsers()
中对 user.hasSystemAccess()
的异常调用。您可以继续执行下一步操作以实现该目的,也可以继续阅读以深入了解 @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);
}
我们只需使用 id
和 username
这两个参数即可构造一个 User
:
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
,并解决下一个问题:在 UseCase.getSystemUsers()
方法中调用 user.hasSystemAccess()
:
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 将显式定义 getter 的签名更改为所提供的名称。
或者,您也可以使用 get:
前缀将其应用于相应属性,如下所示:
@get:JvmName("hasSystemAccess")
val hasSystemAccess
get() = "sys" in groups
备用方法对于使用默认的隐式定义 getter 的属性特别有用。例如:
@get:JvmName("isActive")
val active: Boolean
这样,您无需显式定义 getter,即可更改 getter 的名称。
虽然有上述区别,但您可以视需要使用任一方式。这两种方式都能让 Kotlin 创建名为 hasSystemAccess()
的 getter。
如果切换回 UseCase.java
,即可验证 getSystemUsers()
现在已不存在任何错误!
下一个错误位于 formatUser()
中,但如果您想深入了解 Kotlin getter 命名惯例,请继续阅读本部分,而暂时不要执行下一步操作。
getter 和 setter 的命名
编写 Kotlin 代码时,很容易忽略的一点是,如果我们编写如下所示的代码:
val myString = "Logged in as ${user.displayName}")
这实际上会调用函数来获取 displayName
的值。若要验证这一点,我们可以在菜单中依次转到 Tools > Kotlin > Show Kotlin Bytecode,然后点击 Decompile 按钮:
String myString = "Logged in as " + user.getDisplayName();
如果要从 Java 访问这些元素,我们需要显式写出相应 getter 的名称。
在大多数情况下,Kotlin 属性的 getter 的 Java 名称就是 get
+ 属性名称,如 User.getHasSystemAccess()
和 User.getDisplayName()
所示。唯一的例外情况是名称以“is”开头的属性。在此示例中,getter 的 Java 名称是 Kotlin 属性的名称。
例如,User
上的属性,例如:
val isAdmin get() = //...
可使用以下代码从 Java 进行访问:
boolean userIsAnAdmin = user.isAdmin();
通过使用 @JvmName
注解,Kotlin 会为带注解的项生成具有指定名称(而非默认名称)的字节码。
对于 setter 也是如此,其生成的名称始终是 set
+ 属性名称。例如,采用以下类:
class Color {
var red = 0f
var green = 0f
var blue = 0f
}
假定我们要将 setter 的名称从 setRed()
更改为 updateRed()
,而不更改 getter。我们可以使用 @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 中,属性通常通过 getter 和 setter 公开。其中包括 val
属性。
若要更改这个行为,可以使用 @JvmField
注解。如果将该注解应用于某个类中的某个属性,Kotlin 就会跳过生成 getter 方法的步骤(对于 var
属性,则跳过生成 setter 的步骤),并让后备字段可供直接访问。
由于 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 还是 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
类型可以是基元,例如 int
、float
和 String
。在此示例中,由于 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.kt
中 Repository.saveAs
的相关代码,就会发现它确实会抛出异常。这是怎么回事?
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
中的编译器错误全都得到修正了!太棒了!
您可能想知道,现在从 Kotlin 调用 saveAs()
时,您是否必须使用 try
和 catch
块。
不需要!请记住,Kotlin 没有受检异常,而且向方法添加 @Throws
并不会改变这种情况:
fun saveFromKotlin(path: String) {
Repository.saveAs(path)
}
在异常能够得到处理时,捕获异常仍是有用的,但 Kotlin 并不强制您处理异常。
在此 Codelab 中,我们介绍了如何编写 Kotlin 代码,同时还要让 Kotlin 代码支持编写惯用的 Java 代码。
我们讨论了如何使用注解来更改 Kotlin 生成 JVM 字节码的方式,例如:
@JvmStatic
用于生成静态成员和方法。@JvmOverloads
用于为包含默认值的函数生成重载方法。@JvmName
用于更改 getter 和 setter 的名称。@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
}
Repository.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
}