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 的长度、宽度和高度。
  • 方法(也称为成员函数)是类的功能。方法是您可以对对象执行的操作。例如,您可以对 Aquarium 对象执行 fillWithWater() 操作。
  • 接口是类可以实现的规范。例如,清洁是除水族馆以外的其他对象的共同特征,并且不同对象的清洁方式通常相似。因此,您可以有一个名为 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 块中。在本步骤中,您将向 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 块按它们在类定义中出现的顺序执行,并且在调用构造函数时,所有 init 块都会执行。

步骤 3:了解辅助构造函数

在本步骤中,您将了解辅助构造函数,并向您的类添加一个辅助构造函数。除了可以包含一个或多个 init 块的主构造函数外,Kotlin 类还可以包含一个或多个辅助构造函数,以允许构造函数重载,即具有不同参数的构造函数。

  1. Aquarium 类中,添加一个辅助构造函数,该构造函数使用 constructor 关键字将鱼的数量作为参数。为根据鱼的数量计算的水族箱体积(以升为单位)创建一个 val tank 属性。假设每条鱼需要 2 升(2,000 立方厘米)水,再加上一点额外的空间,以防止水溢出。
constructor(numberOfFish: Int) : this() {
    // 2,000 cm^3 per fish + extra room so water doesn't spill
    val tank = numberOfFish * 2000 * 1.1
}
  1. 在辅助构造函数中,保持长度和宽度(在主构造函数中设置)相同,并计算使水族箱达到给定体积所需的 height。
    // 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,该方法根据提供的水量重新计算 height。按照惯例,setter 参数的名称为 value,但如果需要,可以更改它。
var volume: Int
    get() = width * height * length / 1000
    set(value) {
        height = (value * 1000) / (width * length)
    }
  1. buildAquarium() 中,添加代码将 Aquarium 的体积设置为 70 升。打印新的尺寸。
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    aquarium6.volume = 70
    aquarium6.printSize()
}
  1. 再次运行程序并观察更改后的 height 和体积。
⇒ 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 中的所有内容都是 public,这意味着所有内容都可以在任何地方访问,包括类、方法、属性和成员变量。

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

  • public 表示在类外部可见。默认情况下,所有内容都是 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 类开放

在此步骤中,使 Aquariumopen,以便你可以在下一步中覆盖它。

  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. 覆盖 volume 属性以计算圆柱体。圆柱体的公式为 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。您需要重写 FishColor 中的 colorFishAction 中的 eat()
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,它接受一个 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 接口,但将实现委托给它们。

a7556eed5ec884a3.png

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

9. 任务:创建一个数据类

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

步骤 1:创建一个数据类

  1. example.myapp 包下添加一个名为 decor 的新包,用于保存新代码。在 项目 窗格中右键单击 example.myapp,然后选择 文件 > 新建 > 包
  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 对象,它们都是 “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. 下一个代码实验室

继续下一课:

要查看课程概述,包括其他代码实验室的链接,请参阅 “面向程序员的 Kotlin 集训营:欢迎来到课程”。