在这个 Codelab 中,您将学习如何将 Java 代码重构为 Kotlin。此外,您还将学习 Kotlin 语言有哪些约定,以及如何确保您编写的代码遵循这些约定。
此 Codelab 适用于任何使用 Java 并考虑将其项目迁移到 Kotlin 的开发者。我们将从几个 Java 类开始,指导您使用 IDE 将它们转换为 Kotlin。然后,我们将检查转换后的代码,研究如何改进它,使其更符合习惯用法,同时避免常见的错误。
您将学到什么
您将学习如何将 Java 重构为 Kotlin。在此过程中,您将学习 Kotlin 语言的以下特性和概念:
- 处理可空性
- 实现单例
- 数据类
- 处理字符串
- Elvis 运算符
- 解构
- 属性和支持属性
- 默认参数和命名参数
- 使用集合
- 扩展函数
- 顶层函数和参数
let
、apply
、with
和run
关键字
假设
您应该熟悉 Java。
您需要什么
创建新项目
如果您使用的是 IntelliJ IDEA,请使用 Kotlin/JVM 创建一个新的 Java 项目。
如果您使用的是 Android Studio,请创建一个不包含 Activity 的新项目。请将语言选项保留为 Kotlin。
选择 Kotlin 作为默认语言,您的项目将自动配置为支持 Kotlin!
代码
我们将创建一个 User
模型对象和一个 Repository
单例类,该类处理 User
对象,并提供接口来输出用户列表和格式化的用户名列表。
在 app/java/<yourpackagename> 下创建一个名为 User.java
的新文件,并将以下代码粘贴到文件中:
public class User {
private String firstName;
private String lastName;
public User(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
创建一个名为 Repository.java
的新文件,并将以下代码粘贴到文件中:
import java.util.ArrayList;
import java.util.List;
public class Repository {
private static final Repository INSTANCE = null;
private List<User> users = null;
public static Repository getInstance() {
if (INSTANCE == null) {
synchronized (Repository.class) {
if (INSTANCE == null) {
INSTANCE = new Repository();
}
}
}
return INSTANCE;
}
// keeping the constructor private to enforce the usage of getInstance
private Repository() {
User user1 = new User("Jane", "");
User user2 = new User("John", null);
User user3 = new User("Anne", "Doe");
users = new ArrayList();
users.add(user1);
users.add(user2);
users.add(user3);
}
public List<User> getUsers() {
return users;
}
public List<String> getFormattedUserNames() {
List<String> userNames = new ArrayList<>(users.size());
for (User user : users) {
String name;
if (user.getLastName() != null) {
if (user.getFirstName() != null) {
name = user.getFirstName() + " " + user.getLastName();
} else {
name = user.getLastName();
}
} else if (user.getFirstName() != null) {
name = user.getFirstName();
} else {
name = "Unknown";
}
userNames.add(name);
}
return userNames;
}
}
IDE 在将 Java 代码自动重构为 Kotlin 代码时可以做得非常好,但有时也需要一些帮助。我们将首先执行重构,然后检查重构后的代码,以便了解它是如何完成的以及为什么以这种方式完成。
转到 User.java
文件,并将其转换为 Kotlin:**代码 -> 将 Java 文件转换为 Kotlin 文件**。
如果 IDE 在转换后提示您进行更正,请点击 **是**。
您应该会看到以下 Kotlin 代码:
class User(var firstName: String?, var lastName: String?)
请注意,User.java
已重命名为 User.kt
。Kotlin 文件的扩展名为 .kt。
Java 的 User
类最初有两个属性:firstName
和 lastName
。这两个属性都有 getter 和 setter 方法,因此属性值是可变的。Kotlin 的可变变量关键字是 var
,因此转换器对这两个属性都使用了 var
。如果 Java 属性只有 getter,则属性值是不可变的,并被声明为 val
变量。val
类似于 Java 中的 final
关键字。
Kotlin 和 Java 之间的一个关键区别是,Kotlin 明确指定变量是否可以接受 null 值。具体来说,它是通过在类型声明后附加“?
”来实现的。
Java 的 User
类中的属性可以接受 null 值,因此这两个属性都使用 String?
标记为可为空。如果您使用 org.jetbrains.annotations.NotNull 或 androidx.annotation.NonNull 将 Java 成员注释为非空,转换器将识别这一点,并在 Kotlin 中将这些字段也设为非空。
现在我们已经完成了基本的重构过程。但是,我们还可以使用更惯用的方式编写代码。让我们来探索一下!
数据类
User
类只存储数据。对于具有此角色的类,Kotlin 提供了一个关键字:data
。当您将一个类标记为 data
类时,编译器将自动创建 getter 和 setter。此外,它还派生 equals()
、hashCode()
和 toString()
函数。
让我们向 User
类添加 data
关键字,如下所示:
data class User(var firstName: String?, var lastName: String?)
与 Java 类似,Kotlin 也可以有一个主构造函数和一个或多个辅助构造函数。上面的构造函数是 User 类的主构造函数。如果您转换具有多个构造函数的 Java 类,转换器也会在 Kotlin 中自动创建多个构造函数。构造函数都使用 constructor
关键字进行定义。
要创建此类的实例,您可以执行以下操作:
val user1 = User("Jane", "Doe")
相等性
Kotlin 有两种类型的相等性:
- 结构相等使用
==
运算符,并调用equals()
来确定两个实例是否相等。 - 引用相等使用 === 运算符,以检查两个引用是否指向同一个对象。
数据类主构造函数中定义的属性将用于检查结构相等性。
val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // false
在 Kotlin 中,我们可以为函数调用中的参数提供默认值。当省略参数时,将使用此默认值。在 Kotlin 中,构造函数也是一种函数,因此我们可以使用默认参数将 lastName
的默认值指定为 null
。为此,我们只需将 null
分配给 lastName
。
data class User(var firstName: String?, var lastName: String? = null)
// usage
val jane = User ("Jane") // same as User("Jane", null)
val joe = User ("John", "Doe")
调用函数时,您可以为函数参数命名,如下所示:
val john = User (firstName = "John", lastName = "Doe")
假设 firstName
使用 null
作为其默认值,而 lastName
则没有。在这种情况下,由于默认参数将位于没有默认值的参数之前,因此您必须使用命名参数来调用此函数,如下所示:
data class User(var firstName: String? = null, var lastName: String?)
// usage
val jane = User (lastName = "Doe") // same as User(null, "Doe")
val john = User ("John", "Doe")
在继续之前,请确保您的 User
类是一个 data
类。让我们将 Repository
类转换为 Kotlin。自动转换的结果应该如下所示:
class Repository
// keeping the constructor private to enforce the usage of getInstance
private constructor() {
private val users: MutableList<User>? = null
val formattedUserNames: List<String>
get() {
val userNames = ArrayList<String>(users!!.size)
for ((firstName, lastName) in users) {
val name: String?
if (lastName != null) {
if (firstName != null) {
name = "$firstName $lastName"
} else {
name = lastName
}
} else if (firstName != null) {
name = firstName
} else {
name = "Unknown"
}
userNames.add(name)
}
return userNames
}
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
users = ArrayList()
users!!.add(user1)
users.add(user2)
users.add(user3)
}
fun getUsers(): List<User>? {
return users
}
companion object {
private var INSTANCE: Repository? = null
val instance: Repository
get() {
if (INSTANCE == null) {
synchronized(Repository::class.java) {
if (INSTANCE == null) {
INSTANCE = Repository()
}
}
}
return INSTANCE
}
}
}
让我们看看自动转换器做了什么:
- 添加了一个
init
代码块 (Repository.kt#L33) static
字段现在位于companion object
代码块中 (Repository.kt#L48)users
列表可以为空,因为在声明时它没有被实例化 (Repository.kt#L9)getFormattedUserNames()
方法现在是一个属性 (Repository.kt#L11)- 在遍历用户列表时,语法与 Java 不同 (Repository.kt#L14)
init 块
在 Kotlin 中,主构造函数不能包含任何代码,因此初始化代码放在 init
块中。但是,它们的功能完全相同。
class Repository private constructor() {
...
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
users = ArrayList()
users!!.add(user1)
users.add(user2)
users.add(user3)
}
}
init
代码主要用于处理属性的初始化。这也可以在声明属性时完成。例如,在 Kotlin 版本的 Repository
类中,我们可以看到 `users` 属性是在声明时初始化的。
private val users: MutableList<User>? = null
Kotlin 的“静态”属性和“静态”方法
在 Java 中,我们使用 static
关键字来指示字段或函数属于类,而不是类的某个实例。因此,我们在 Repository
类中创建了 INSTANCE
静态字段。在 Kotlin 中,companion object
代码块与此等效。您也可以在此处声明静态字段和静态函数。转换器已经创建了 INSTANCE
字段并将其移动到这里。
实现单例
因为我们只需要一个 Repository
类的实例,所以我们在 Java 中使用了单例模式。在 Kotlin 中,通过将 class
关键字替换为 object
,我们可以在编译器级别强制执行此模式。现在,我们可以删除私有构造函数和伴生对象(companion object
)。
object Repository {
private val users: MutableList<User>? = null
val formattedUserNames: List<String>
get() {
val userNames = ArrayList<String>(users!!.size)
for ((firstName, lastName) in users) {
val name: String?
if (lastName != null) {
if (firstName != null) {
name = "$firstName $lastName"
} else {
name = lastName
}
} else if (firstName != null) {
name = firstName
} else {
name = "Unknown"
}
userNames.add(name)
}
return userNames
}
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
users = ArrayList()
users!!.add(user1)
users.add(user2)
users.add(user3)
}
fun getUsers(): List<User>? {
return users
}
}
使用 object
类时,我们直接在对象上调用函数和属性,如下所示:
val users = Repository.users
解构
Kotlin 允许使用称为解构声明的语法将对象分解成多个变量。我们可以创建多个变量,并且可以独立使用它们。
例如,数据类支持解构,因此自动转换器能够解构 for
循环中的 User
对象。这使我们能够直接处理 firstName
和 lastName
值,如下所示:
for ((firstName, lastName) in users) {
val name: String?
if (lastName != null) {
if (firstName != null) {
name = "$firstName $lastName"
}
...
当将 Repository
类转换为 Kotlin 时,自动转换器已将用户列表设为可为空,因为我们在声明时没有将其初始化为对象。在 users
对象的所有使用中,我们都使用了非空断言运算符 !!
。此运算符将所有变量转换为非空类型,并在值为 null 时抛出异常。使用 !!
会有在运行时抛出异常的风险。
建议您使用以下方法之一来处理可空性:
- 执行 null 检查 (
if (users != null) {...}
) - 使用 Elvis 运算符
?:
(稍后将在 Codelab 中介绍) - 使用一些 Kotlin 标准函数(稍后将在 Codelab 中介绍)
在本例中,我们知道用户列表不需要可为空,因为在对象构建完成后它会立即初始化,因此我们可以在声明它时直接实例化它。
当创建集合类实例时,您可以利用 Kotlin 提供的许多帮助器函数,使您的代码更易于阅读和更灵活。在本例中,我们使用 MutableList
用于 users
,如下所示:
private val users: MutableList<User>? = null
为了简单起见,我们可以使用 mutableListOf()
函数,提供列表元素类型,从 `init` 代码块中删除 `ArrayList` 构造函数调用,并删除 `users` 属性的显式类型声明,如下所示:
private val users = mutableListOf<User>()
进行此更改后,users
属性现在不再为 null,因此我们可以移除所有不必要的 !!
运算符实例。
由于 lastName
和 firstName
都可能为 null
,因此在构建格式化的用户名列表时,我们需要处理可空性。自动转换器已将名称变量设置为可为空,但由于我们需要在任何名称缺失时显示 "Unknown"
,所以我们可以从类型声明中移除 ?
,从而使名称变为非空。
val name: String
如果 lastName
为 null,则 name
将为 firstName
或 "Unknown"
,如下所示:
if (lastName != null) {
if (firstName != null) {
name = "$firstName $lastName"
} else {
name = lastName
}
} else if (firstName != null) {
name = firstName
} else {
name = "Unknown"
}
通过使用 Elvis 运算符 ?:
,我们可以以更惯用的方式编写代码。如果左侧表达式不为 null,则 Elvis 运算符将返回该表达式,否则将返回右侧表达式。
因此,如果 user.firstName
不为 null,以下代码将返回该值。如果 user.firstName
为 null,该表达式将返回右侧值 "Unknown"
,如下所示:
if (lastName != null) {
...
} else {
name = firstName ?: "Unknown"
}
接下来,我们将在 formattedUserNames 的
get 方法中也使用 Elvis 运算符:
val formattedUserNames: List<String>
get() {
val userNames = ArrayList<String>(users.size)
for ((firstName, lastName) in users) {
val name: String
if (lastName != null) {
if (firstName != null) {
name = "$firstName $lastName"
} else {
name = lastName ?: "Unknown"
}
} else {
name = firstName ?: "Unknown"
}
userNames.add(name)
}
return userNames
}
借助字符串模板,Kotlin 可以简化 String
的处理。字符串模板允许在字符串声明中引用变量。
自动转换器已更新了名字和姓氏的连接表达式,允许用户使用 $
符号直接在字符串中引用变量名,并将表达式放在 { }
中。
// Java
name = user.getFirstName() + " " + user.getLastName();
// Kotlin
name = "${user.firstName} ${user.lastName}"
在 Kotlin 中,if
、when
、for
和 while
都是表达式,它们有返回值。IDE 甚至会发出警告,指出赋值应该从 if
中移除,如下所示:
我们将遵循 IDE 的建议,将两个 if
语句的赋值都移出。if 语句的最后一行将用于赋值。如下所示,我们可以更清楚地看到此代码块的唯一目的是初始化 name 值:
name = if (firstName != null) {
// do something
firstName
}
// name = firstName
接下来,页面会弹出一个警告,提示我们可以将 name
的声明和赋值合并。同样,我们将继续遵循该警告。由于可以推断出名称变量的类型,因此我们可以移除显式类型声明。现在,formattedUserNames
将如下所示:
val formattedUserNames: List<String>
get() {
val userNames = ArrayList<String>(users.size)
for ((firstName, lastName) in users) {
val name = if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName ?: "Unknown"
}
} else {
firstName ?: "Unknown"
}
userNames.add(name)
}
return userNames
}
让我们深入了解 formattedUserNames
的 get 方法,看看如何使它更符合我们的习惯。此代码将执行以下操作:
- 创建一个新的字符串列表
- 循环遍历用户列表
- 根据用户的姓名和姓氏构建每个用户的格式化姓名
- 返回新创建的列表
val formattedUserNames: List<String>
get() {
val userNames = ArrayList<String>(users.size)
for ((firstName, lastName) in users) {
val name = if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName ?: "Unknown"
}
} else {
firstName ?: "Unknown"
}
userNames.add(name)
}
return userNames
}
Kotlin 提供各种集合转换,它们扩展了 Java Collections API 的功能,从而使开发更快、更安全。其中一个转换是 map
函数。此函数将返回一个新列表,其中包含对原始数组中的每个元素应用指定转换函数的结果。因此,我们不必手动创建新的列表并迭代用户列表,而是可以使用 map
函数并替换 map
内部 for 循环中的逻辑。默认情况下,map
中使用的当前列表项的名称为 it
,但是为了提高可读性,您可以将其替换为您自己的变量名。在本例中,让我们将其命名为 user
:
val formattedUserNames: List<String>
get() {
return users.map { user ->
val name = if (user.lastName != null) {
if (user.firstName != null) {
"${user.firstName} ${user.lastName}"
} else {
user.lastName ?: "Unknown"
}
} else {
user.firstName ?: "Unknown"
}
name
}
}
为了进一步简化,我们还可以完全移除 name
变量,如下所示:
val formattedUserNames: List<String>
get() {
return users.map { user ->
if (user.lastName != null) {
if (user.firstName != null) {
"${user.firstName} ${user.lastName}"
} else {
user.lastName ?: "Unknown"
}
} else {
user.firstName ?: "Unknown"
}
}
}
我们已经看到,自动转换器将 getFormattedUserNames()
函数替换为名为 formattedUserNames
的属性,该属性具有自定义 getter。在幕后,Kotlin 仍然生成 getFormattedUserNames()
方法,该方法将返回 List
。
在 Java 中,我们通过 getter 和 setter 函数公开类属性。Kotlin 允许我们更好地对属性和功能(类可以执行的操作)进行分类,前者表示为字段,后者表示为函数。在本例中,Repository 类非常简单;它不执行任何操作,因此只包含字段。
现在,当调用 formattedUserNames
Kotlin 属性的 getter 时,将触发之前在 Java getFormattedUserNames()
函数中触发的逻辑。
虽然我们没有与 formattedUserNames
属性完全对应的字段,但 Kotlin 提供了一个名为 field
的自动支持字段,我们还可以从自定义 getter 和 setter(如果需要)中访问它。
但是,有时我们还需要自动支持字段无法提供的某些额外功能。以下是一个示例。
Repository
类有一个可变的用户列表,它在 getUsers
函数中公开,该函数由 Java 代码生成,如下所示:
fun getUsers(): List<User>? { return users }
但这里有一个问题:由于返回了 users
,任何使用 Repository 类的用户都可以修改用户列表,这不是一个好主意!我们将使用支持属性来解决这个问题。
首先,将 users
重命名为 _users
。然后添加一个公共的不可变属性,它将返回用户列表。将其命名为 users
,如下所示:
private val _users = mutableListOf<User>() val users: List<User> get() = _users
进行此更改后,私有的 _users
属性将成为公共 users
属性的支持属性。在 Repository
类外部,由于数据类的用户只能通过 users
访问 _users
列表,因此该列表是不可变的。
Repository
类目前实现了如何为 User
对象计算格式化的用户名。但是,如果我们需要在其他类中重复使用相同的格式化逻辑,我们需要复制粘贴该逻辑或将其移动到 User
类。
Kotlin 允许在任何类、对象或接口的外部声明函数和属性。例如,用于创建新的 List
实例的 mutableListOf()
函数直接在标准库中的 Collections.kt
中定义。
在 Java 中,每当我们需要一些实用程序功能时,我们通常会创建一个 Util
类并将该功能声明为静态函数。在 Kotlin 中,您可以声明顶层函数,而无需使用类。但是,Kotlin 还支持创建扩展函数。这些函数扩展了特定类型,但它们是在该类型外部声明的。因此,它们与该类型相关。
您可以使用可见性修饰符来限制扩展函数和扩展属性的可见性。这些修饰符仅向需要扩展的类公开扩展功能,并且不会污染命名空间。
对于 User
类,我们可以添加一个扩展函数来计算格式化的名称,或者将格式化的名称存储在扩展属性中。我们可以在 Repository
类的外部但在同一文件中添加此扩展函数,如下所示:
// extension function fun User.getFormattedName(): String { return if (lastName != null) { if (firstName != null) { "$firstName $lastName" } else { lastName ?: "Unknown" } } else { firstName ?: "Unknown" } } // extension property val User.userFormattedName: String get() { return if (lastName != null) { if (firstName != null) { "$firstName $lastName" } else { lastName ?: "Unknown" } } else { firstName ?: "Unknown" } } // usage: val user = User(...) val name = user.getFormattedName() val formattedName = user.userFormattedName
然后,我们可以使用这些扩展函数和扩展属性,就好像它们是 User
类的一部分一样。
由于格式化的名称是 User
的属性,而不是 Repository
类的功能,因此我们应该使用扩展属性。Repository
文件现在如下所示:
val User.formattedName: String get() { return if (lastName != null) { if (firstName != null) { "$firstName $lastName" } else { lastName ?: "Unknown" } } else { firstName ?: "Unknown" } } object Repository { private val _users = mutableListOf<User>() val users: List<User> get() = _users val formattedUserNames: List<String> get() { return _users.map { user -> user.formattedName } } init { val user1 = User("Jane", "") val user2 = User("John", null) val user3 = User("Anne", "Doe") _users.add(user1) _users.add(user2) _users.add(user3) } }
Kotlin 标准库使用扩展函数来扩展多个 Java API 的功能;Iterable
和 Collection
上的许多功能都是作为扩展函数实现的。例如,我们在上一步中使用的 map
函数是 Iterable
上的一个扩展函数。
在 Repository
类代码中,我们将多个 User
对象添加到 _users
列表中。借助作用域函数,我们可以以更符合习惯的方式进行这些函数调用。
为了只在特定对象的上下文中执行代码,而无需根据名称访问该对象,Kotlin 创建了 5 个作用域函数:let
、apply
、with
、run
和 also
。这些函数既简洁又强大,它们都有一个接收器 (this
),可以带参数 (it
),并且可能返回一个值。您可以根据您想要实现的目标来选择使用哪个函数。
以下便捷备忘单可以帮助您记住:
由于我们需要在 Repository
中配置 _users
对象,我们可以使用 apply
函数使代码更符合习惯,如下所示:
init {
val user1 = User("Jane", "")
val user2 = User("John", null)
val user3 = User("Anne", "Doe")
_users.apply {
// this == _users
add(user1)
add(user2)
add(user3)
}
}
在此 Codelab 中,我们介绍了在将 Java 代码重构为 Kotlin 时需要了解的基础知识。这些重构工作与开发平台无关,并且有助于确保您以惯用的方式编写代码。
Kotlin 是符合习惯的,它可以帮助您编写简洁和流畅的代码。利用 Kotlin 提供的所有功能,您可以通过多种方式提高代码的安全、简洁和可读性。例如,我们还可以进一步优化 Repository
类,直接在声明中通过用户对 _users
列表进行实例化,从而避免使用 init
块,如下所示:
private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
我们介绍了许多主题,包括处理可空性、单例、字符串和集合,以及扩展函数、顶层函数、属性和作用域函数等。我们已经将两个 Java 类重构为两个 Kotlin 类,如下所示:
User.kt
class User(var firstName: String?, var lastName: String?)
Repository.kt
val User.formattedName: String
get() {
return if (lastName != null) {
if (firstName != null) {
"$firstName $lastName"
} else {
lastName ?: "Unknown"
}
} else {
firstName ?: "Unknown"
}
}
object Repository {
private val _users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
val users: List<User>
get() = users
val formattedUserNames: List<String>
get() {
_users.map { user -> user.formattedName }
}
}
以下是 Java 功能及其在 Kotlin 中的对应关系概述:
Java | Kotlin |
|
|
|
|
|
|
仅包含数据的类 |
|
构造函数中的初始化 |
|
| 在 |
单例类 |
|
有关 Kotlin 以及如何在您的开发平台上使用它的更多信息,请参阅以下资源:
- Kotlin Koans
- Kotlin 教程
- 使用 Kotlin 开发 Android 应用 - 免费课程
- 面向 Java 开发者的 Kotlin - 正在审核中的免费课程