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

1. 欢迎

此 Codelab 是 Kotlin 程序员训练营课程 的一部分。如果您按顺序完成这些 Codelab,将能从中获得最大的价值。根据您的知识水平,您可能可以略过某些部分。本课程面向熟悉面向对象语言并希望学习 Kotlin 的程序员。

sEioGm-YJlcEfGjX0S6M-MQDi23k2ZjQCNPkuImT4e5BIqCJ7XCoLqvDJlUK4cB9XfffJQOcpcW_I8J1LRpYN6qk_b7NMWSQi_0yAWk6Gm5e9C-vvNo5v8geG9iINqKPc_byPxgqMA

简介

在本 Codelab 中,您将创建一个 Kotlin 程序,并学习 Kotlin 中的类和对象。如果您熟悉其他面向对象语言,则本内容中的大部分内容您都会觉得熟悉,但 Kotlin 有一些重要的区别,可以减少您需要编写的代码量。您还将学习抽象类和接口委托。

本课程中的教程并非构建单个示例应用,而是旨在扩展您的知识,并且彼此之间相对独立,以便您可以略过您已熟悉的章节。为了将它们联系起来,许多示例都使用了水族馆主题。如果您想了解完整的“水族馆”故事,请查看 Kotlin 程序员训练营 Udacity 课程。

BLgezynJ_92kR7lbZPbmkh7cDUCFMm3Ugo_JUOdDd5IpMdkk8nu3nbMiSkQWK5dx4-NX4qlbUXwU9l_Pj_7QqoRSUX2YbiddIUO9I100elofv-IY6xAHo7RL9CCXnjEwBKyLknPHzw

您应该掌握的知识

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

您将学到的内容

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

您将执行的操作

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

2. 术语

以下编程术语您应该已经熟悉

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

3. 任务:创建类

在此任务中,您将创建一个新包以及一个具有某些属性和方法的类。

步骤 1:创建包

包可以帮助您整理代码。

  1. 在**项目**窗格中,在**Hello Kotlin** 项目下,右键单击**src**文件夹。
  2. 选择**新建 > 包**,并将其命名为 example.myapp

步骤 2:创建具有属性的类

类使用关键字 class 定义,并且类名按照约定以大写字母开头。

  1. 右键单击**example.myapp**包。
  2. 选择**新建 > Kotlin 文件/类**。
  3. 在**类型**下,选择**类**,并将类命名为**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. 在左侧的**项目**窗格中,右键单击**example.myapp**包。
  2. 选择**新建 > Kotlin 文件/类**。
  3. 在**类型**下拉列表中,保持选择为**文件**,并将文件命名为 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 

4. 任务:添加类构造函数

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

步骤 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 块中。在此步骤中,您将一些 init 块添加到 Aquarium 类。

  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 型的 tank 属性,用于根据鱼的数量计算水族箱的容积(以升为单位)。假设每条鱼需要 2 升(2000 立方厘米)的水,再加上一点额外的空间,以防止水溢出。
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

5. 概念:了解可见性修饰符

到目前为止,代码中还没有使用可见性修饰符,例如 publicprivate。这是因为在 Kotlin 中,默认情况下,所有内容都是公共的,这意味着所有内容都可以在任何地方访问,包括类、方法、属性和成员变量。

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

  • public 表示在类外部可见。默认情况下,所有内容都是公共的,包括类的变量和方法。
  • internal 表示它仅在该模块内可见。模块 是编译在一起的一组 Kotlin 文件,例如库或应用程序。
  • private 表示它仅在该类(或如果您正在使用函数,则为源文件)中可见。
  • protectedprivate 相同,但它也对任何子类可见。

有关更多信息,请参阅 Kotlin 文档中的 可见性修饰符

成员变量

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

如果您希望代码可以读取或写入的属性,但外部代码只能读取,您可以将属性及其 getter 保留为 public,并将 setter 声明为 private,如下所示。

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

6. 任务:了解子类和继承

在此任务中,您将学习 Kotlin 中子类和继承的工作原理。它们与您在其他语言中看到的类似,但有一些区别。

在 Kotlin 中,默认情况下,类不能被子类化。类似地,属性和成员变量不能被子类覆盖(尽管可以访问它们)。

您必须将类标记为 open 才能允许它被子类化。类似地,您必须将属性和成员变量标记为 open,以便在子类中覆盖它们。open 关键字是必需的,以防止意外地将实现细节作为类接口的一部分泄露。

步骤 1:使 Aquarium 类变为 open

在此步骤中,您将 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. 添加一个 open 的 shape 属性,其值为 "rectangle"
   open val shape = "rectangle"
  1. 添加一个 open 的 water 属性,其 getter 返回 Aquarium 容积的 90%。
    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 = 40Aquarium
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. 创建 Aquarium 的一个子类,名为 TowerTank,它实现圆柱形水箱而不是矩形水箱。您可以在 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)

7. 任务:比较抽象类和接口

有时您希望定义一些相关类之间共享的通用行为或属性。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. 创建 AquariumFish 的两个子类:SharkPlecostomus

  2. 由于color是抽象的,因此子类必须实现它。将Shark设置为灰色,并将Plecostomus设置为金色。
class Shark: AquariumFish() {
    override val color = "gray"
}

class Plecostomus: AquariumFish() {
    override val color = "gold"
}
  1. 在**main.kt**中,创建一个makeFish()函数来测试你的类。实例化一个Shark和一个Plecostomus,然后打印每个鱼的颜色。
  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的子类。

A diagram showing the abstract class, AquariumFish, and two subclasses, Shark and Plecostumus.

步骤 2. 创建一个接口

  1. 在**AquariumFish.kt**中,创建一个名为FishAction的接口,其中包含一个方法eat()
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接口。

e4b747e0303bcdaa.png

何时使用抽象类与接口

上面的例子很简单,但是当你有许多相互关联的类时,抽象类和接口可以帮助你保持设计的简洁、更有条理以及更容易维护。

如上所述,抽象类可以有构造函数,而接口不能有,但在其他方面它们非常相似。那么,你应该何时使用每个呢?

当你使用接口来组合一个类时,该类的功能通过它包含的类实例来扩展。与从抽象类继承相比,组合往往使代码更容易重用和推理。此外,你可以在一个类中使用多个接口,但你只能从一个抽象类继承。

组合通常会导致更好的封装、更低的耦合(相互依赖)、更简洁的接口和更易用的代码。由于这些原因,使用接口进行组合是首选设计。另一方面,从抽象类继承往往适合某些问题。所以你应该优先选择组合,但是当继承有意义时,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")
}

8. 任务:使用接口委托

前面的任务介绍了抽象类、接口和组合的概念。接口委托是一种高级技术,其中接口的方法由一个辅助(或委托)对象实现,然后由类使用。当你在一系列不相关的类中使用接口时,此技术非常有用:你将所需接口功能添加到一个单独的辅助类中,并且每个类都使用辅助类的实例来实现该功能。

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

步骤 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来实现此操作,从而创建委托。这意味着,它不是实现FishColor,而是使用GoldColor提供的实现。因此,每次访问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**中,创建一个PrintingFishAction类,该类实现FishAction,它接收一个String类型的food,然后打印鱼吃的东西。
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接口,但将实现委托给它们。

a7556eed5ec884a3.png

接口委托功能强大,并且在其他语言中可能使用抽象类时,你通常应该考虑如何使用它。它允许你使用组合来插入行为,而不是需要许多专门化方式的子类。

9. 任务:创建数据类

数据类类似于某些其他语言中的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() 函数来创建一个 Decoration 实例并打印,该实例的装饰材料为 "granite"
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. 添加一个 main() 函数来调用 makeDecorations(),并运行你的程序。注意由于这是一个数据类,所以会生成有意义的输出。
⇒ Decoration(rocks=granite)
  1. makeDecorations() 中,再实例化两个 Decoration 对象,它们都为 "slate",并打印它们。
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)

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

    val decoration3 = Decoration("slate")
    println(decoration3)
}
  1. makeDecorations() 中,添加一个打印语句,打印 decoration1decoration2 比较的结果,以及 decoration3decoration2 比较的结果。使用数据类提供的 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

10. 任务:了解单例、枚举和密封类

在本任务中,你将学习 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 返回成功或错误

11. 总结

本课涵盖了很多内容。虽然其中很多内容在其他面向对象编程语言中应该很熟悉,但 Kotlin 添加了一些功能来使代码简洁易读。

类和构造函数

  • 使用 class 在 Kotlin 中定义类。
  • 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 定义枚举。

抽象类、接口和委托

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

12. 了解更多

Kotlin 文档

如果你想了解更多有关本课程中任何主题的信息,或者如果你遇到困难,https://kotlinlang.org 是你最好的起点。

Kotlin 教程

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

Udacity 课程

要查看有关此主题的 Udacity 课程,请参阅 程序员的 Kotlin 集训营

IntelliJ IDEA

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

13. 作业

本节列出了作为课程一部分完成此代码实验室的学生的一些可能的作业。教师需要执行以下操作

  • 根据需要分配作业。
  • 告知学生如何提交作业。
  • 批改作业。

教师可以根据需要使用这些建议,并且可以随意布置他们认为合适的任何其他作业。

如果你自己正在学习此代码实验室,可以随意使用这些作业来测试你的知识。

回答以下问题

问题 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 类。

14. 下一个 Codelab

继续下一课:

有关课程概述,包括指向其他 Codelab 的链接,请参阅 “面向程序员的 Kotlin 集训营:欢迎参加课程。”