Kotlin 编程人员训练营 4:面向对象的编程

此 Codelab 是面向编程人员的 Kotlin 训练营课程的一部分。如果您按顺序学习这些 Codelab,您将会充分发掘此课程的价值。根据您的知识水平,您也许可以浏览某些部分。本课程专为了解面向对象的语言并希望学习 Kotlin 的编程人员而设计。

简介

在此 Codelab 中,您将创建一个 Kotlin 程序并了解 Kotlin 中的类和对象。如果您知道另一种面向对象的语言,那么您会非常熟悉此内容,但 Kotlin 有一些重要差异,可以减少您需要编写的代码量。您还将了解抽象类和接口委托。

本课程不是构建单个示例应用,而是用来学习知识,但它们是半独立的,以便您能够浏览已熟悉的部分。为了将两者联系起来,许多示例使用了水族馆主题。如果您想看完整的水族馆故事,请查看面向编程人员的 Kotlin 训练营 Udacity 课程。

您应当已掌握的内容

  • Kotlin 基础知识,包括类型、运算符和循环
  • Kotlin 的函数语法
  • 面向对象编程的基础知识
  • IDE 基础知识,例如 IntelliJ IDEA 或 Android Studio

学习内容

  • 如何在 Kotlin 中创建类和访问属性
  • 如何在 Kotlin 中创建和使用类构造函数
  • 如何创建子类以及继承的工作原理
  • 关于抽象类、接口和接口委托
  • 如何创建和使用数据类
  • 如何使用单例、枚举和密封类

您将执行的操作

  • 创建具有属性的类
  • 为类创建构造函数
  • 创建子类
  • 查看抽象类和接口的示例
  • 创建简单的数据类
  • 了解单例、枚举和密封类

您应当已熟悉以下编程术语:

  • 类是指对象的蓝图。例如,Aquarium 类就是用于制作水族馆对象的蓝图。
  • 对象是类的实例;水族馆对象是一个实际的 Aquarium
  • 属性是指类的特征,例如 Aquarium 的长度、宽度和高度。
  • 方法(也称为成员函数)是指类的功能。方法是指您可以对对象“执行”的操作。例如,您可以对 Aquarium 执行 fillWithWater() 操作。
  • 接口是指类可以实现的规范。例如,清洁对于水族箱以外的其他对象十分常见,并且对于不同的对象,清洁通常都以类似的方式进行。因此,您可以设置一个名为 Clean 的接口,用于定义 clean() 方法。Aquarium 类可以实现 Clean 接口,以使用软海绵清洁水族箱。
  • 软件包是指一种用于将相关代码分组以使其井然有序或构建代码库的方式。创建软件包后,您可以将软件包的内容导入另一个文件,并重复使用其中的代码和类。

在此任务中,您将创建一个新的软件包和一个包含某些属性及一种方法的类。

第 1 步:创建一个软件包

利用软件包可以使代码保持井然有序。

  1. Project 窗格的 Hello Kotlin 项目下,右键点击 src 文件夹。
  2. 依次选择 New > Package,并将其命名为 example.myapp

第 2 步:创建一个具有某些属性的类

类使用关键字 class 定义,按照惯例,类名称以大写字母开头。

  1. 右键点击 example.myapp 软件包。
  2. 依次选择 New > Kotlin File / Class
  3. Kind 下,选择 Class,并将此类命名为 Aquarium。IntelliJ IDEA 会在文件中包含软件包名称,并创建一个空的 Aquarium 类。
  4. Aquarium 类中,定义并初始化宽度、高度和长度(以厘米为单位)的 var 属性。使用默认值初始化属性。
package example.myapp

class Aquarium {
    var width: Int = 20
    var height: Int = 40
    var length: Int = 100
}

在后台,Kotlin 会自动为您在 Aquarium 类中定义的属性创建 getter 和 setter,因此您可以直接访问属性,例如 myAquarium.length

第 3 步:创建一个 main() 函数

创建一个名为 main.kt 的新文件来保存 main() 函数。

  1. 在左侧的 Project 窗格中,右键点击 example.myapp 软件包。
  2. 依次选择 New > Kotlin File / Class
  3. Kind 下拉列表中,将选项保存为 File,并将此文件命名为 main.kt。IntelliJ IDEA 会包含软件包名称,但不包含文件的类定义。
  4. 定义 buildAquarium() 函数,并在其中创建一个 Aquarium 实例。如需创建实例,请引用类,就像将其用作函数 Aquarium() 一样。这会调用类的构造函数并创建一个 Aquarium 类的实例,类似于使用其他语言中的 new
  5. 定义 main() 函数并调用 buildAquarium()
package example.myapp

fun buildAquarium() {
    val myAquarium = Aquarium()
}

fun main() {
    buildAquarium()
}

第 4 步:添加一种方法

  1. Aquarium 类中,添加一种用于输出水族箱尺寸属性的方法。
    fun printSize() {
        println("Width: $width cm " +
                "Length: $length cm " +
                "Height: $height cm ")
    }
  1. main.kt 中的 buildAquarium() 内,对 myAquarium 调用 printSize() 方法。
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
}
  1. 点击 main() 函数旁边的绿色三角形以运行程序,并观察结果。
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
  1. buildAquarium() 中,添加代码以将高度设置为 60,并输出更改后的尺寸属性。
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
    myAquarium.height = 60
    myAquarium.printSize()
}
  1. 运行程序并观察输出结果。
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 100 cm Height: 60 cm 

在此任务中,您将为类创建一个构造函数,并继续使用属性。

第 1 步:创建一个构造函数

在此步骤中,您将向在第一个任务中创建的 Aquarium 类添加一个构造函数。在前面的示例中,Aquarium 的每个实例都是使用相同的尺寸创建的。创建实例后,您可以通过设置属性来更改实例的尺寸,但是从一开始就使用正确的尺寸创建实例会更加简单。

在某些编程语言中,构造函数是通过在类中创建的与类同名的方法来定义的。在 Kotlin 中,您可以直接在类声明中定义构造函数,在圆括号内指定参数,就像该类是一种方法一样。至于 Kotlin 中的函数,这些参数也可以包含默认值。

  1. 在您之前创建的 Aquarium 类中,更改类定义以包含三个具有 lengthwidthheight 默认值的构造函数参数,并将它们赋予相应的属性。
class Aquarium(length: Int = 100, width: Int = 20, height: Int = 40) {
   // Dimensions in cm
   var length: Int = length
   var width: Int = width
   var height: Int = height
...
}
  1. 更紧凑的 Kotlin 方法是使用 varval 通过该构造函数直接定义属性,并且 Kotlin 还会自动创建 getter 和 setter。然后,您可以移除该类正文中的属性定义。
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
  1. 使用该构造函数创建 Aquarium 对象时,您可以不指定任何参数并获取默认值,也可以仅指定部分参数或指定全部参数并创建完全自定义大小的 Aquarium。在 buildAquarium() 函数中,尝试以不同方式使用命名参数创建 Aquarium 对象。
fun buildAquarium() {
    val aquarium1 = Aquarium()
    aquarium1.printSize()
    // default height and length
    val aquarium2 = Aquarium(width = 25)
    aquarium2.printSize()
    // default width
    val aquarium3 = Aquarium(height = 35, length = 110)
    aquarium3.printSize()
    // everything custom
    val aquarium4 = Aquarium(width = 25, height = 35, length = 110)
    aquarium4.printSize()
}
  1. 运行程序并观察输出结果。
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 25 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 110 cm Height: 35 cm 
Width: 25 cm Length: 110 cm Height: 35 cm 

请注意,您不必重载构造函数并为上述每种案例(以及其他组合的一些其他案例)分别编写一个不同的版本。Kotlin 会根据默认值和命名参数创建所需内容。

第 2 步:添加 init 块

上面的示例构造函数仅声明属性并为其赋予表达式的值。如果您的构造函数需要更多初始化代码,您可以将其放在一个或多个 init 块中。在此步骤中,您将向 Aquarium 类添加一些 init 块。

  1. Aquarium 类中,添加一个 init 对象块以输出对象正在初始化,然后添加第二个块以输出以体积表示的音量。
class Aquarium (var length: Int = 100, var width: Int = 20, var height: Int = 40) {
    init {
        println("aquarium initializing")
    }
    init {
        // 1 liter = 1000 cm^3
        println("Volume: ${width * length * height / 1000} l")
    }
}
  1. 运行程序并观察输出结果。
aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 100 l
Width: 25 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 77 l
Width: 20 cm Length: 110 cm Height: 35 cm 
aquarium initializing
Volume: 96 l
Width: 25 cm Length: 110 cm Height: 35 cm 

请注意,系统会按照 init 块在类定义中显示的顺序执行这些块,并在调用构造函数时执行所有这些块。

第 3 步:了解次构造函数

在此步骤中,您将了解次构造函数,并将其中一个次构造函数添加到类中。除了主构造函数(可以有一个或多个 init 块)之外,Kotlin 类还可以有一个或多个辅助构造函数(以允许具有不同参数的构造函数),以允许构造函数过载。

  1. Aquarium 类中,使用 constructor 关键字添加将鱼类数量作为其参数的次构造函数。根据鱼类数量,为水族箱的计算体积(以升为单位)创建 val 水箱属性。假设每条鱼 2 升 (2000 cm^3) 水,并且额外预留一些空间,这样水就不会溢出了。
constructor(numberOfFish: Int) : this() {
    // 2,000 cm^3 per fish + extra room so water doesn't spill
    val tank = numberOfFish * 2000 * 1.1
}
  1. 在次构造函数中,确保长度和宽度(在主构造函数中设置)相同,并计算使水箱达到给定体积所需的高度。
    // calculate the height needed
    height = (tank / (length * width)).toInt()
  1. buildAquarium() 函数中,添加调用以使用新的次构造函数创建 Aquarium。输出尺寸和体积。
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
  1. 运行程序并观察输出结果。
⇒ aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

请注意,体积将被输出两次,一次在次构造函数执行之前由主构造函数中的 init 块输出,另一次由 buildAquarium() 中的代码输出。

此外,您还可以在主构造函数中添加 constructor 关键字,但在大多数情况下并不需要这样做。

第 4 步:添加一个新属性 getter

在此步骤中,您将添加一个显式属性 getter。Kotlin 会在您定义属性时自动定义 getter 和 setter。但是,有时需要调整或计算某个属性的值。例如,在上面的示例中,您输出了 Aquarium 的卷。您可以通过为该体积定义变量和 getter,使该体积可以用作属性。由于需要计算 volume,因此 getter 需要返回计算后的值,您可以使用单行函数执行此操作。

  1. Aquarium 类中,定义一个名为 volumeInt 属性,并在下一行中定义 get() 方法来计算体积。
val volume: Int
    get() = width * height * length / 1000  // 1000 cm^3 = 1 l
  1. 移除用于输出体积的 init 块。
  2. 移除 buildAquarium() 中用于输出体积的代码。
  3. printSize() 方法中,添加一行来输出体积。
fun printSize() {
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm "
    )
    // 1 l = 1000 cm^3
    println("Volume: $volume l")
}
  1. 运行程序并观察输出结果。
⇒ aquarium initializing
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

尺寸和体积和之前一样,但仅在通过主构造函数和次构造函数完全初始化对象后,系统才会输出一次体积。

第 5 步:添加一个属性 setter

在此步骤中,您将为体积创建一个新属性 setter。

  1. Aquarium 类中,将 volume 更改为 var,以便可以进行多次设置。
  2. 通过在 getter 下面添加 set() 方法,为 volume 属性添加 setter,这会根据提供的水量重新计算高度。按照惯例,setter 参数的名称是 value,但您可以根据需要进行更改。
var volume: Int
    get() = width * height * length / 1000
    set(value) {
        height = (value * 1000) / (width * length)
    }
  1. buildAquarium() 中,添加代码以将水族箱的体积设置为 70 升。输出新尺寸。
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    aquarium6.volume = 70
    aquarium6.printSize()
}
  1. 再次运行该程序,并观察更改后的高度和体积。
⇒ aquarium initialized
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l
Width: 20 cm Length: 100 cm Height: 35 cm 
Volume: 70 l

截至目前为止,代码中始终不存在可见性修饰符,如 publicprivate。这是因为,默认情况下,Kotlin 中的所有内容都是公开的,这意味着用户可以在任何位置访问所有内容,包括类、方法、属性和成员变量。

在 Kotlin 中,类、对象、接口、构造函数、函数、属性及其 setter 可以具有可见性修饰符:

  • public 意味着在该类外可见。默认情况下,所有内容(包括该类的变量和方法)都是公开的。
  • internal 意味着它将仅在该模块中可见。模块是一组编译在一起的 Kotlin 文件,例如库或应用。
  • private 意味着将仅在该类(或源文件,如果您使用函数)中可见。
  • protectedprivate 一样,但还将对任何子类可见。

如需了解详情,请参阅 Kotlin 文档中的可见性修饰符

成员变量

默认情况下,类中的属性或成员变量是 public。如果您使用 var 定义这些属性或成员变量,它们是可变的,即可读写。如果您使用 val 定义这些属性或成员变量,在初始化后,它们是只读的。

如果您想要代码可以读取或写入的属性,但外部代码只能读取,那么您可以将属性及其 getter 保留为公开状态,并将 setter 声明为私有,如下所示。

var volume: Int
    get() = width * height * length / 1000
    private set(value) {
        height = (value * 1000) / (width * length)
    }

在此任务中,您将了解子类和继承在 Kotlin 中的运作方式,与您在其他语言中看到的类似,但又有一些区别。

在 Kotlin 中,默认情况下,不能为类创建子类。同样,子类无法替换属性和成员变量(尽管可以访问它们)。

您必须将某个类标记为 open 才能对其进行子类化。同样,您必须将属性和成员变量标记为 open,才能在子类中替换它们。必须提供 open 关键字,以防止意外泄露实现细节,放入类接口。

第 1 步:将 Aquarium 类设置为公开

在此步骤中,您将 Aquarium 类设置为 open,以便在下一步中替换该类。

  1. 使用 open 关键字标记 Aquarium 类及其所有属性。
open class Aquarium (open var length: Int = 100, open var width: Int = 20, open var height: Int = 40) {
    open var volume: Int
        get() = width * height * length / 1000
        set(value) {
            height = (value * 1000) / (width * length)
        }
  1. 添加一个开放性 shape 属性,其具有值 "rectangle"
   open val shape = "rectangle"
  1. 添加一个开放性 water 属性,其具有返回 Aquarium 体积的 90% 的 getter。
    open var water: Double = 0.0
        get() = volume * 0.9
  1. 将代码添加到 printSize() 方法以输出形状和水量(以体积的百分比表示)。
fun printSize() {
    println(shape)
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm ")
    // 1 l = 1000 cm^3
    println("Volume: $volume l Water: $water l (${water/volume*100.0}% full)")
}
  1. buildAquarium() 中,更改代码以使用 width = 25length = 25height = 40 创建 Aquarium
fun buildAquarium() {
    val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
    aquarium6.printSize()
}
  1. 运行程序并观察新输出结果。
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)

第 2 步:创建一个子类

  1. 创建一个名为 TowerTankAquarium 的子类,用于实现圆柱形水箱,而不是矩形水箱。您可以在 Aquarium 下面添加 TowerTank,因为您可以在与 Aquarium 类相同的文件中添加其他类。
  2. TowerTank 中,替换在构造函数中定义的 height 属性。如需替换属性,请在子类中使用 override 关键字。
  1. 使 TowerTank 的构造函数采用 diameter。调用 Aquarium 父类中的构造函数时,请将 diameter 同时用于 lengthwidth
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
  1. 替换体积属性以计算圆柱形。圆柱形的计算公式是圆周率 (PI) 乘以半径的平方再乘以高度。您需要从 java.lang.Math 导入常量 PI
    override var volume: Int
    // ellipse area = π * r1 * r2
    get() = (width/2 * length/2 * height / 1000 * PI).toInt()
    set(value) {
        height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
    }
  1. TowerTank 中,替换 water 属性,使其占体积的 80%。
override var water = volume * 0.8
  1. shape 替换为 "cylinder"
override val shape = "cylinder"
  1. 最终 TowerTank 类应该如以下代码所示。

Aquarium.kt

package example.myapp

import java.lang.Math.PI

... // existing Aquarium class

class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
    override var volume: Int
    // ellipse area = π * r1 * r2
    get() = (width/2 * length/2 * height / 1000 * PI).toInt()
    set(value) {
        height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
    }

    override var water = volume * 0.8
    override val shape = "cylinder"
}
  1. buildAquarium() 中,创建一个直径为 25 厘米、高度为 45 厘米的 TowerTank。输出尺寸。

main.kt:

package example.myapp

fun buildAquarium() {
    val myAquarium = Aquarium(width = 25, length = 25, height = 40)
    myAquarium.printSize()
    val myTower = TowerTank(diameter = 25, height = 40)
    myTower.printSize()
}
  1. 运行程序并观察输出结果。
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)
aquarium initializing
cylinder
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 18 l Water: 14.4 l (80.0% full)

有时,需要定义在一些相关类中共享的共有行为或属性。Kotlin 提供接口和抽象类这两种方法来实现这一点。在此任务中,您将为所有鱼类共有的属性创建一个抽象的 AquariumFish 类。创建一个名为 FishAction 的接口来定义所有鱼类共有的行为。

  • 抽象类和接口都不能单独实例化,这意味着您无法直接创建这些类型的对象。
  • 抽象类有构造函数。
  • 接口不能有任何构造函数逻辑或存储任何状态。

第 1 步:创建一个抽象类

  1. example.myapp 下,创建一个新文件 AquariumFish.kt
  2. 创建一个类(也称为 AquariumFish),并使用 abstract 对其进行标记。
  3. 添加一个 String 属性 color,并使用 abstract 进行标记。
package example.myapp

abstract class AquariumFish {
    abstract val color: String
}
  1. 创建 AquariumFishSharkPlecostomus 的两个子类。
  2. 由于 color 是抽象类,因此子类必须实现它。将Shark设为灰色和Plecostomus金色。
class Shark: AquariumFish() {
    override val color = "gray"
}

class Plecostomus: AquariumFish() {
    override val color = "gold"
}
  1. main.kt 中,创建一个 makeFish() 函数来测试您的类。实例化 SharkPlecostomus,然后输出二者的颜色。
  2. 删除 main() 中较早的测试代码,并添加对 makeFish() 的调用。您的代码应该如以下代码所示。

main.kt

package example.myapp

fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()

    println("Shark: ${shark.color}")
    println("Plecostomus: ${pleco.color}")
}

fun main () {
    makeFish()
}
  1. 运行程序并观察输出结果。
⇒ Shark: gray 
Plecostomus: gold

下图显示了 Shark 类和 Plecostomus 类,它们是抽象类 AquariumFish 的子类。

展示抽象类 AquariumFish 与两个子类 Shark 和 Plecostumus 的图表。

第 2 步:创建一个接口

  1. AquariumFish.kt 中,使用方法 eat() 创建一个名为 FishAction 的接口。
interface FishAction  {
    fun eat()
}
  1. 向每个子类添加 FishAction,并通过它输出鱼类的行为实现 eat()
class Shark: AquariumFish(), FishAction {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}

class Plecostomus: AquariumFish(), FishAction {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. makeFish() 函数中,通过调用 eat() 让您创建的每条鱼都吃东西。
fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()
    println("Shark: ${shark.color}")
    shark.eat()
    println("Plecostomus: ${pleco.color}")
    pleco.eat()
}
  1. 运行程序并观察输出结果。
⇒ Shark: gray
hunt and eat fish
Plecostomus: gold
eat algae

下图显示了 Shark 类和 Plecostomus 类,它们由 FishAction 接口组成和实现。

抽象类与接口的使用条件

上面的示例很简单,但当您有许多相互关联的类时,抽象类和接口可以帮助您使设计更简洁、更有条理且更易于维护。

如上所述,抽象类可以有构造函数,接口不能,但在其他方面都非常相似。两者的使用条件是什么?

当您使用接口组合类时,该类的功能会通过其包含的类实例进行扩展。与使抽象类继承而来,组合往往使代码更容易重复使用和推理。此外,您可以在一个类中使用多个接口,但只能从一个抽象类创建子类。

组合通常可带来更好的封装、降低耦合(相互依赖)、更简洁的接口和更易用的代码。出于这些原因,首选设计方式便是结合使用组合与接口。另一方面,对于某些问题,从抽象类继承往往较为适合。因此,建议您首选组合方式,但在继承有效的情况下,在 Kotlin 中,您也可以采用从抽象类继承的方式!

  • 如果您有大量方法以及一个或两个默认实现,请使用接口,例如下面的 AquariumAction
interface AquariumAction {
    fun eat()
    fun jump()
    fun clean()
    fun catchFish()
    fun swim()  {
        println("swim")
    }
}
  • 无法完成某个类时,请使用抽象类。例如,返回到 AquariumFish 类,可以使所有 AquariumFish 实现 FishAction,并为 eat 提供默认实现,同时保留 color 作为抽象类,因为鱼类实际上没有默认颜色。
interface FishAction  {
    fun eat()
}

abstract class AquariumFish: FishAction {
   abstract val color: String
   override fun eat() = println("yum")
}

上一个任务引入了抽象类、接口和组合概念。接口委托是一种高级技术,采用这种方法,接口的方法由帮助程序(或代理)对象实现,然后由类使用。当您在一系列不相关的类中使用接口时,此方法很有用:您将所需的接口功能添加到单独的帮助程序类中,并且每个类都使用帮助程序类的实例来实现该功能。

在此任务中,您将使用接口委托向类添加功能。

第 1 步:创建一个新接口

  1. AquariumFish.kt 中,移除 AquariumFish 类。PlecostomusShark 将实现鱼类行动和鱼类颜色的接口,而不是继承自 AquariumFish 类。
  2. 创建一个新接口 FishColor,用于将颜色定义为字符串。
interface FishColor {
    val color: String
}
  1. 更改 Plecostomus 以实现 FishActionFishColor 这两个接口。您需要替换来自 FishColorcolor 以及来自 FishActioneat()
class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. 更改 Shark 类,以便同时实现 FishActionFishColor 这两个接口,而不是从 AquariumFish 继承。
class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}
  1. 完成后的代码应该如下所示:
package example.myapp

interface FishAction {
    fun eat()
}

interface FishColor {
    val color: String
}

class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}

class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}

第 2 步:创建一个单例类

接下来,创建一个实现 FishColor 的帮助程序类,以实现委托部分的设置。创建一个名为 GoldColor 的基本类,用于实现 FishColor - 仅指出其颜色为金色。

创建多个 GoldColor 实例没有任何意义,因为所有这些实例将执行完全相同的操作。因此,在 Kotlin 中,您可以声明一个类,在该类中,您只能使用关键字 object 而非 class 来创建该类的一个实例。Kotlin 将创建一个实例,该实例按类名称引用。然后,所有其他对象只能使用以下实例 - 无法创建此类的其他实例。如果您熟悉单例模式,这就是在 Kotlin 中实现单例的方式。

  1. AquariumFish.kt 中,为 GoldColor 创建一个对象。替换颜色。
object GoldColor : FishColor {
   override val color = "gold"
}

第 3 步:为 FishColor 添加接口委托

现在,您可以使用接口委托。

  1. AquariumFish.kt 中,从 Plecostomus 中移除 color 的替换项。
  2. 更改 Plecostomus 类,以从 GoldColor 获取颜色。为此,您可以将 by GoldColor 添加到类声明中,从而创建委托。换句话说,请使用由 GoldColor 提供的实现,而不是实现 FishColor。因此,每次访问 color 时,系统都会将其委托给 GoldColor
class Plecostomus:  FishAction, FishColor by GoldColor {
   override fun eat() {
       println("eat algae")
   }
}

和原样一样,所有 Plecos 是金色的,但这些鱼实际上有五颜六色。如需解决此问题,您可以添加颜色的构造函数参数,其中将 GoldColor 用作 Plecostomus 的默认颜色。

  1. 更改 Plecostomus 类,以采用传递的 fishColor 及其构造函数,并将其默认值设置为 GoldColor。将委托从 by GoldColor 更改为 by fishColor
class Plecostomus(fishColor: FishColor = GoldColor):  FishAction,
       FishColor by fishColor {
   override fun eat() {
       println("eat algae")
   }
}

第 4 步:为 FishAction 添加接口委托

同样,您可以对 FishAction 使用接口委托。

  1. AquariumFish.kt 中,创建一个用于实现 FishActionPrintingFishAction 类,该类接受一个 Stringfood,然后输出鱼吃什么。
class PrintingFishAction(val food: String) : FishAction {
    override fun eat() {
        println(food)
    }
}
  1. Plecostomus 类中,移除替换函数 eat(),以替换为委托。
  2. Plecostomus 的声明中,将 FishAction 委托给 PrintingFishAction,同时传递 "eat algae"
  3. 完成所有委托后,Plecostomus 类的正文中就没有代码,因此请移除 {},因为所有替换值都由接口委托处理
class Plecostomus (fishColor: FishColor = GoldColor):
        FishAction by PrintingFishAction("eat algae"),
        FishColor by fishColor

下图显示了 SharkPlecostomus 类,它们都由 PrintingFishActionFishColor 接口组成,但会将实现委托给它们。

接口委托功能非常强大,在可能会使用其他语言的抽象类时,您通常应当考虑如何使用该接口委托。利用接口委托可以使用组合来插入行为,而无需大量子类,其中每个子类以不同的方式设为专用类。

数据类与一些其他语言中的 struct 类似,主要用于存储某些数据,但数据类对象仍然是一个对象。Kotlin 数据类对象有一些额外的优势,例如用于打印和复制的实用程序。在此任务中,您将创建一个简单的数据类,并了解 Kotlin 为数据类提供的支持。

第 1 步:创建一个数据类

  1. example.myapp 软件包下添加新软件包 decor,以保存新代码。右键点击 Project 窗格中的 example.myapp,然后依次选择 File > New > Package
  2. 在该软件包中,新建一个名为 Decoration 的类。
package example.myapp.decor

class Decoration {
}
  1. 如需将 Decoration 设为数据类,请在类声明前面加上关键字 data 作为前缀。
  2. 添加一个名为 rocksString 属性,以为该类提供一些数据。
data class Decoration(val rocks: String) {
}
  1. 在文件中的该类之外,添加一个 makeDecorations() 函数,以使用 "granite" 创建并输出 Decoration 的实例。
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. 添加一个 main() 函数以调用 makeDecorations(),然后运行程序。由于创建的合理输出是数据类,因此请予以留意。
⇒ Decoration(rocks=granite)
  1. makeDecorations() 中,实例化另外两个均为“可选”的 Decoration 对象并输出它们。
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)

    val decoration2 = Decoration("slate")
    println(decoration2)

    val decoration3 = Decoration("slate")
    println(decoration3)
}
  1. makeDecorations() 中,添加一个输出语句,用于输出 decoration1decoration2 的比较结果,以及 decoration3decoration2 的比较结果。使用由 data 类提供的 equals() 方法。
    println (decoration1.equals(decoration2))
    println (decoration3.equals(decoration2))
  1. 运行您的代码。
⇒ Decoration(rocks=granite)
Decoration(rocks=slate)
Decoration(rocks=slate)
false
true

第 2 步:使用解构

如需获取数据对象的属性并将其赋给变量,您可以一次赋予一个,如下所示。

val rock = decoration.rock
val wood = decoration.wood
val diver = decoration.diver

相反,您可以为每个属性创建一个变量,并将该数据对象赋给变量组。在 Kotlin 中,为每个变量赋予属性值,

val (rock, wood, diver) = decoration

这称为“解构”,是一种实用的简写形式。变量的数目应与属性的数目一致,并且变量的赋值顺序与它们在类中的声明顺序一致。以下是您可以在 Decoration.kt 中尝试的完整示例。

// Here is a data class with 3 properties.
data class Decoration2(val rocks: String, val wood: String, val diver: String){
}

fun makeDecorations() {
    val d5 = Decoration2("crystal", "wood", "diver")
    println(d5)

// Assign all properties to variables.
    val (rock, wood, diver) = d5
    println(rock)
    println(wood)
    println(diver)
}
⇒ Decoration2(rocks=crystal, wood=wood, diver=diver)
crystal
wood
diver

如果不需要一个或多个属性,则可以使用 _ 而不是变量名称来跳过此类属性,如下面的代码所示。

    val (rock, _, diver) = d5

在此任务中,您将了解 Kotlin 中的一些特殊用途类,包括:

  • 单例类
  • 枚举
  • 密封类

第 1 步:重新调用单例类

回想一下,之前使用 GoldColor 类的示例。

object GoldColor : FishColor {
   override val color = "gold"
}

由于 GoldColor 的每个实例都会执行相同的操作,因此系统将其声明为 object 而不是 class,以使其成为单例。该类只能有一个实例。

第 2 步:创建一个枚举

Kotlin 还支持枚举,让您可以枚举某些内容并按名称引用内容,这与其他语言很相似。通过在声明前面加上关键字 enum 来声明枚举。虽然基本枚举声明仅需名称列表,但您也可以定义与每个名称相关联的一个或多个字段。

  1. Decoration.kt 中,尝试枚举示例。
enum class Color(val rgb: Int) {
   RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}

枚举有点类似于单例,枚举中只能有一个,而枚举中只有一个值。例如,只能有一个 Color.RED、一个 Color.GREEN 和一个 Color.BLUE。在此示例中,为 rgb 属性赋予 RGB 值,以表示颜色分量。您还可以使用 ordinal 属性获取枚举的序数值,使用 name 属性获取其名称。

  1. 尝试其他枚举示例。
enum class Direction(val degrees: Int) {
    NORTH(0), SOUTH(180), EAST(90), WEST(270)
}

fun main() {
    println(Direction.EAST.name)
    println(Direction.EAST.ordinal)
    println(Direction.EAST.degrees)
}
⇒ EAST
2
90

第 3 步:创建密封类

密封类是指可以被子类化的类,但只能在其声明的文件内。如果您尝试在其他文件中创建子类的子类,则会收到错误。

由于类和子类位于同一文件中,因此 Kotlin 将静态地了解所有子类。也就是说,编译时,编译器会查看所有类和子类,并且知道这些类及其子类可以执行额外的检查。

  1. AquariumFish.kt 中,尝试一个采用水上主题的密封类示例。
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()

fun matchSeal(seal: Seal): String {
   return when(seal) {
       is Walrus -> "walrus"
       is SeaLion -> "sea lion"
   }
}

不能在其他文件中为 Seal 类创建子类。如果要添加更多 Seal 类型,必须在同一文件中进行添加。这使得密封类成为表示固定数量类型的安全方式。例如,密封类适用于通过网络 API 返回成功或错误

本课涵盖内容广泛。虽然其他面向对象的编程语言应当熟悉本课大部分内容,但 Kotlin 增加了一些功能以保持代码简洁、可读。

类和构造函数

  • 在 Kotlin 中,使用 class 定义一个类。
  • Kotlin 会自动为属性创建 setter 和 getter。
  • 直接在类定义中定义主构造函数。例如:
    class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
  • 如果主构造函数需要其他代码,请将其编写在一个或多个 init 块中。
  • 类可以使用 constructor 定义一个或多个次构造函数,但 Kotlin 样式是使用工厂函数。

可见性修饰符和子类

  • 在 Kotlin 中,默认情况下,所有类和函数都是 public,但您可以使用修饰符将可见性更改为 internalprivateprotected
  • 如需创建子类,必须将父类标记为 open
  • 如需替换子类中的方法和属性,必须将父类中的方法和属性标记为 open
  • 密封类只能在定义它的同一文件中子类化。通过在声明前面加上 sealed 前缀来创建一个密封类。

数据类、单例和枚举

  • 通过在声明前面加上 data 作为前缀,创建一个数据类。
  • 解构是用于将 data 对象的属性赋给单独变量的简写形式。
  • 通过使用 object 而不是 class,创建一个单例类。
  • 使用 enum class 定义枚举。

抽象类、接口和委托

  • 抽象类和接口是在类之间共享共有行为的两种方法。
  • 抽象类用于定义属性和行为,但将实现留给子类。
  • 接口用于定义行为,并且可以为部分或全部行为提供默认实现。
  • 当您使用接口组合类时,该类的功能会通过其包含的类实例进行扩展。
  • 接口委托使用组合,但也会将实现委托给接口类。
  • 组合是使用接口委托向类添加功能的一种强大方式。通常情况下,组合是首选方式,但对于某些问题,从抽象类继承更为合适。

Kotlin 文档

如果您想详细了解本课程中的任何主题,或者遇到问题,最好访问 https://kotlinlang.org

Kotlin 教程

https://try.kotlinlang.org 网站包含丰富的名为 Kotlin Koans 的教程,一种基于网络的解释器,以及一套完整的示例参考文档。

Udacity 课程

如需查看有关此主题的 Udacity 课程,请参阅面向编程人员的 Kotlin 训练营

IntelliJ IDEA

您可以在 JetBrains 网站上找到 IntelliJ IDEA 文档

此部分列出了在由讲师主导的课程中,学生学习此 Codelab 后可能需要完成的家庭作业。讲师自行决定是否执行以下操作:

  • 根据需要布置作业。
  • 告知学生如何提交家庭作业。
  • 给家庭作业评分。

讲师可以酌情采纳这些建议,并且可以自由布置自己认为合适的任何其他家庭作业。

如果您是在自学此 Codelab,可随时通过这些家庭作业来检测您的知识掌握情况。

回答以下问题

问题 1

类有一个特殊方法,可作为创建该类的对象的蓝图。调用了什么方法?

▢ 构建器

▢ 实例化器

▢ 构造函数

▢ 蓝图

问题 2

在以下关于接口和抽象类的表述中,哪一项是不正确的?

▢ 抽象类可以有构造函数。

▢ 接口不能包含构造函数。

▢ 接口和抽象类可以直接实例化。

▢ 抽象属性必须由抽象类的子类实现。

问题 3

以下哪一项不是属性、方法等的 Kotlin 可见性修饰符?

internal

nosubclass

protected

private

问题 4

以数据类为例:
data class Fish(val name: String, val species:String, val colors:String)
以下哪一项不是用于创建和解构 Fish 对象的有效代码?

val (name1, species1, colors1) = Fish("Pat", "Plecostomus", "gold")

val (name2, _, colors2) = Fish("Bitey", "shark", "gray")

val (name3, species3, _) = Fish("Amy", "angelfish", "blue and black stripes")

val (name4, species4, colors4) = Fish("Harry", "halibut")

问题 5

假设您拥有一座动物园,里面有许多动物,这些动物都需要照顾。以下哪一项不属于实施看护的行为?

▢ 用于动物吃的不同类型的食物的 interface

▢ 一个 abstract Caretaker 类,您可以在其中创建不同类型的看护人员。

▢ 为动物提供干净水的 interface

▢ 投放时间表条目的 data 类。

继续学习下一课:5.1 扩展程序

如需大致了解本课程(包括指向其他 Codelab 的链接),请参阅面向编程人员的 Kotlin 训练营:欢迎学习本课程