Refactoring to 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:Code -> Convert Java File to Kotlin File

若 IDE 在转换后提示更正,请点击 Yes

您应会看到以下 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? 标记为可为 null。若您使用 org.jetbrains.annotations.NotNull 或 androidx.annotation.NonNull 将 Java 成员标注为非 null,转换器将会识别这一情况,并在 Kotlin 中将这些字段同样设为非 null。

此时我们已完成基本的重构流程。不过,我们还可以使用更惯常的方式编写代码。下面就让我们一探究竟!

数据类

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

假如 firstNamenull 用作其默认值,而 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 列表可为 null,因为该对象在声明时并未实例化 (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 时,自动转换器已将用户列表设为可为 null,这是由于我们在声明时并未将其初始化为对象。在 users 对象的所有使用情境中,我们都使用了非 null 断言运算符 !!。该运算符可将所有变量转换为非 null 类型,并在值为 null 时抛出异常。使用 !! 时,存在运行时抛出异常的风险。

建议您使用下列其中一种方法来处理可空性:

  • 执行 null 检查 (if (users != null) {...})
  • 使用 Elvis 运算符 ?:(稍后将在 Codelab 中阐述)
  • 使用某些 Kotlin 标准函数(稍后将在 Codelab 中阐述)

本例中,我们了解用户列表无需设为可为 null,因为其会在对象构建完成后立即进行初始化,如此我们便可在声明该对象时直接将其实例化。

创建集合类实例时,您可以利用 Kotlin 所提供的多个帮助程序函数,让代码更易阅读而且更为灵活。本例中,我们将 MutableList 用于 users,具体如下:

private val users: MutableList<User>? = null

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

private val users = mutableListOf<User>()

作出此项更改后,users 属性现已变为非 null,我们此时亦可移除所有不必要的 !! 运算符实例。

由于 lastNamefirstName 均可为 null,因此在构建经过格式化的用户名列表时,我们需要处理可空性。自动转换器已将名称变量设为可为 null,但由于要在任一名称缺失时显示 "Unknown",所以我们可从类型声明中移除 ?,从而使名称变为非 null。

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,但为便于阅读,您可将 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 专门创建了 5 个作用域函数,即: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 以及如何将其用在您的开发平台上,请参阅下列资源: