Kotlin 重构 (重构为 Kotlin)

在这个 Codelab 中,您将学习如何将 Java 代码重构为 Kotlin。此外,您还将学习 Kotlin 语言的约定,以及如何确保您编写的代码遵循这些约定。

这个 Codelab 适用于任何使用 Java 并考虑将其项目迁移到 Kotlin 的开发者。我们将从几个 Java 类开始,指导您使用 IDE 将它们转换为 Kotlin。然后,我们将检查转换后的代码,看看如何改进它以使其更符合惯例,同时避免常见的错误。

您将学习的内容

您将学习如何将 Java 重构为 Kotlin。在此过程中,您将学习 Kotlin 语言的以下特性和概念:

  • 处理可空性
  • 实现单例
  • 数据类
  • 处理字符串
  • Elvis 运算符
  • 解构
  • 属性和后备属性
  • 默认参数和命名参数
  • 使用集合
  • 扩展函数
  • 顶层函数和参数
  • letapplywithrun 关键字

假设

您应该熟悉 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 类有两个属性:firstNamelastName。这两个属性都有 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 对象。这允许我们直接访问 firstNamelastName 值,如下所示:

 
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 中介绍)

在本例中,我们了解到用户列表无需设置为可为空(nullable),因为它会在对象构建完成后立即初始化。因此,我们可以在声明对象时直接实例化它。

创建集合类实例时,您可以利用 Kotlin 提供的多个辅助函数,使代码更易读且更灵活。在本例中,我们将 MutableList 用于 users,如下所示:

private val users: MutableList<User>? = null

为简便起见,我们可以使用 mutableListOf() 函数,提供列表元素类型,从 init 代码块中移除 ArrayList 构造函数调用,然后移除 users 属性的显式类型声明,如下所示:

private val users = mutableListOf<User>()

进行此更改后,users 属性现在是非空的,我们也可以移除所有不必要的 !! 运算符实例。

由于 lastNamefirstName 都可能是 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 中,ifwhenforwhile 都是表达式,它们有返回值。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 的功能;IterableCollection 上的许多功能都以扩展函数的形式实现。例如,我们在上一步中使用的 map 函数是 Iterable 上的扩展函数。

Repository 类代码中,我们将多个 User 对象添加到 _users 列表中。使用作用域函数,我们可以更惯用地进行这些函数调用。

为了在特定对象的上下文中执行代码,而无需根据名称访问该对象,Kotlin 提供了五个作用域函数:letapplywithrunalso。这些函数简短而强大,它们都有一个接收器(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

final 对象

val 对象

equals()

==

==

===

仅包含数据的类

data

构造函数中的初始化

init 块中的初始化

static 字段和函数

companion object 中声明的字段和函数

单例类

object

要了解更多关于 Kotlin 以及如何在您的开发平台上使用它的信息,请参阅以下资源: