1. 欢迎
此 Codelab 是 Kotlin 程序员训练营课程 的一部分。如果您按顺序完成这些 Codelab,将能从中获得最大的价值。根据您的知识水平,您可能可以略过某些部分。本课程面向熟悉面向对象语言并希望学习 Kotlin 的程序员。
简介
在本 Codelab 中,您将创建一个 Kotlin 程序,并学习 Kotlin 中的类和对象。如果您熟悉其他面向对象语言,则本内容中的大部分内容您都会觉得熟悉,但 Kotlin 有一些重要的区别,可以减少您需要编写的代码量。您还将学习抽象类和接口委托。
本课程中的教程并非构建单个示例应用,而是旨在扩展您的知识,并且彼此之间相对独立,以便您可以略过您已熟悉的章节。为了将它们联系起来,许多示例都使用了水族馆主题。如果您想了解完整的“水族馆”故事,请查看 Kotlin 程序员训练营 Udacity 课程。
您应该掌握的知识
- Kotlin 基础知识,包括类型、运算符和循环
- Kotlin 的函数语法
- 面向对象编程的基础知识
- IDE(如 IntelliJ IDEA 或 Android Studio)的基础知识
您将学到的内容
- 如何在 Kotlin 中创建类和访问属性
- 如何在 Kotlin 中创建和使用类构造函数
- 如何创建子类以及继承的工作原理
- 关于抽象类、接口和接口委托
- 如何创建和使用数据类
- 如何使用单例、枚举和密封类
您将执行的操作
- 创建一个具有属性的类
- 为类创建构造函数
- 创建子类
- 检查抽象类和接口的示例
- 创建一个简单的数据类
- 了解单例、枚举和密封类
2. 术语
以下编程术语您应该已经熟悉
- 类是对象的蓝图。例如,
Aquarium
类是创建水族馆对象的蓝图。 - 对象是类的实例;水族馆对象是一个实际的
Aquarium
。 - 属性是类的特征,例如
Aquarium
的长度、宽度和高度。 - 方法(也称为成员函数)是类的功能。方法是您可以对对象执行的操作。例如,您可以
fillWithWater()
一个Aquarium
对象。 - 接口是类可以实现的规范。例如,清洁是除水族馆之外的其他对象的常见操作,并且不同对象的清洁方式通常相似。因此,您可以有一个名为
Clean
的接口,该接口定义了一个clean()
方法。Aquarium
类可以实现Clean
接口以用软海绵清洁水族馆。 - 包是用于对相关代码进行分组以保持其井然有序或创建代码库的一种方式。创建包后,您可以将包的内容导入到另一个文件中,并在其中重用代码和类。
3. 任务:创建类
在此任务中,您将创建一个新包以及一个具有某些属性和方法的类。
步骤 1:创建包
包可以帮助您整理代码。
- 在**项目**窗格中,在**Hello Kotlin** 项目下,右键单击**src**文件夹。
- 选择**新建 > 包**,并将其命名为
example.myapp
。
步骤 2:创建具有属性的类
类使用关键字 class
定义,并且类名按照约定以大写字母开头。
- 右键单击**example.myapp**包。
- 选择**新建 > Kotlin 文件/类**。
- 在**类型**下,选择**类**,并将类命名为**
Aquarium
**。IntelliJ IDEA 会在文件中包含包名称,并为您创建一个空的Aquarium
类。 - 在
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()
函数。
- 在左侧的**项目**窗格中,右键单击**example.myapp**包。
- 选择**新建 > Kotlin 文件/类**。
- 在**类型**下拉列表中,保持选择为**文件**,并将文件命名为
main.kt
。IntelliJ IDEA 会包含包名称,但不会为文件包含类定义。 - 定义一个
buildAquarium()
函数,并在其中创建Aquarium
的实例。要创建实例,请像引用函数一样引用类,Aquarium()
。这将调用类的构造函数并创建Aquarium
类的实例,类似于在其他语言中使用new
。 - 定义一个
main()
函数并调用buildAquarium()
。
package example.myapp
fun buildAquarium() {
val myAquarium = Aquarium()
}
fun main() {
buildAquarium()
}
步骤 4:添加方法
- 在
Aquarium
类中,添加一个方法来打印水族馆的尺寸属性。
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
}
- 在
main.kt
中,在buildAquarium()
中,对myAquarium
调用printSize()
方法。
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
}
- 通过单击
main()
函数旁边的绿色三角形来运行程序。观察结果。
⇒ Width: 20 cm Length: 100 cm Height: 40 cm
- 在
buildAquarium()
中,添加代码以将高度设置为 60 并打印已更改的尺寸属性。
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
myAquarium.height = 60
myAquarium.printSize()
}
- 运行程序并观察输出。
⇒ Width: 20 cm Length: 100 cm Height: 40 cm Width: 20 cm Length: 100 cm Height: 60 cm
4. 任务:添加类构造函数
在此任务中,您将为类创建构造函数,并继续使用属性。
步骤 1:创建构造函数
在此步骤中,您将向在第一个任务中创建的 Aquarium
类添加构造函数。在前面的示例中,每个 Aquarium
实例都是使用相同的尺寸创建的。您可以在创建后通过设置属性来更改尺寸,但从一开始就创建正确的尺寸会更简单。
在某些编程语言中,构造函数是通过在类中创建与类名称相同的名称的方法来定义的。在 Kotlin 中,您直接在类声明本身中定义构造函数,将参数指定在括号内,就像类是一个方法一样。与 Kotlin 中的函数一样,这些参数可以包含默认值。
- 在您之前创建的
Aquarium
类中,更改类定义以包含三个带默认值的构造函数参数length
、width
和height
,并将它们分配给相应的属性。
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
...
}
- 更简洁的 Kotlin 方法是使用
var
或val
直接在构造函数中定义属性,Kotlin 还会自动创建 getter 和 setter。然后,您可以删除类主体中的属性定义。
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
- 使用该构造函数创建
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()
}
- 运行程序并观察输出。
⇒ 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
类。
- 在
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")
}
}
- 运行程序并观察输出。
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 类还可以有一个或多个辅助构造函数,以允许构造函数重载,即具有不同参数的构造函数。
- 在
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
}
- 在辅助构造函数内部,保持长度和宽度(在主构造函数中设置)不变,并计算使水箱达到指定容积所需的高度。
// calculate the height needed
height = (tank / (length * width)).toInt()
- 在
buildAquarium()
函数中,添加一个调用,使用新的辅助构造函数创建一个Aquarium
对象。打印尺寸和容积。
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
- 运行程序并观察输出。
⇒ 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 需要返回计算出的值,您可以使用单行函数来实现。
- 在
Aquarium
类中,定义一个名为volume
的Int
型属性,并定义一个get()
方法,在下一行计算容积。
val volume: Int
get() = width * height * length / 1000 // 1000 cm^3 = 1 l
- 删除打印容积的
init
代码块。 - 删除
buildAquarium()
中打印容积的代码。 - 在
printSize()
方法中,添加一行代码以打印容积。
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm "
)
// 1 l = 1000 cm^3
println("Volume: $volume l")
}
- 运行程序并观察输出。
⇒ aquarium initializing Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 l
尺寸和容积与之前相同,但容积只在主构造函数和辅助构造函数都完全初始化对象后打印一次。
步骤 5:添加属性 setter
在此步骤中,您将为容积创建一个新的属性 setter。
- 在
Aquarium
类中,将volume
更改为var
,以便可以多次设置它。 - 通过在 getter 下方添加
set()
方法为volume
属性添加一个 setter,该方法根据提供的用水量重新计算高度。按照惯例,setter 参数的名称为value
,但您可以根据需要更改它。
var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}
- 在
buildAquarium()
中,添加代码以将水族箱的容积设置为 70 升。打印新的尺寸。
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
aquarium6.volume = 70
aquarium6.printSize()
}
- 再次运行程序并观察更改后的高度和容积。
⇒ 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. 概念:了解可见性修饰符
到目前为止,代码中还没有使用可见性修饰符,例如 public
或 private
。这是因为在 Kotlin 中,默认情况下,所有内容都是公共的,这意味着所有内容都可以在任何地方访问,包括类、方法、属性和成员变量。
在 Kotlin 中,类、对象、接口、构造函数、函数、属性及其 setter 可以具有可见性修饰符。
public
表示在类外部可见。默认情况下,所有内容都是公共的,包括类的变量和方法。internal
表示它仅在该模块内可见。模块 是编译在一起的一组 Kotlin 文件,例如库或应用程序。private
表示它仅在该类(或如果您正在使用函数,则为源文件)中可见。protected
与private
相同,但它也对任何子类可见。
有关更多信息,请参阅 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
,以便您可以在下一步中覆盖它。
- 使用
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)
}
- 添加一个 open 的
shape
属性,其值为"rectangle"
。
open val shape = "rectangle"
- 添加一个 open 的
water
属性,其 getter 返回Aquarium
容积的 90%。
open var water: Double = 0.0
get() = volume * 0.9
- 在
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)")
}
- 在
buildAquarium()
中,更改代码以创建width = 25
、length = 25
和height = 40
的Aquarium
。
fun buildAquarium() {
val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
aquarium6.printSize()
}
- 运行程序并观察新的输出。
⇒ aquarium initializing rectangle Width: 25 cm Length: 25 cm Height: 40 cm Volume: 25 l Water: 22.5 l (90.0% full)
步骤 2:创建子类
- 创建
Aquarium
的一个子类,名为TowerTank
,它实现圆柱形水箱而不是矩形水箱。您可以在Aquarium
下添加TowerTank
,因为您可以在与Aquarium
类相同的文件中添加另一个类。 - 在
TowerTank
中,覆盖在构造函数中定义的height
属性。要覆盖属性,请在子类中使用override
关键字。
- 使
TowerTank
的构造函数接受一个diameter
。在调用Aquarium
超类的构造函数时,使用diameter
作为length
和width
。
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
- 覆盖容积属性以计算圆柱体的容积。圆柱体的公式为 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()
}
- 在
TowerTank
中,覆盖water
属性,使其为容积的 80%。
override var water = volume * 0.8
- 将
shape
覆盖为"cylinder"
。
override val shape = "cylinder"
- 您的最终
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"
}
- 在
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()
}
- 运行程序并观察输出。
⇒ 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. 创建抽象类
- 在 example.myapp 下,创建一个新文件
AquariumFish.kt
。 - 创建一个类,也称为
AquariumFish
,并使用abstract
标记它。 - 添加一个
String
型属性color
,并使用abstract
标记它。
package example.myapp
abstract class AquariumFish {
abstract val color: String
}
- 创建
AquariumFish
的两个子类:Shark
和Plecostomus
。 - 由于
color
是抽象的,因此子类必须实现它。将Shark
设置为灰色,并将Plecostomus
设置为金色。
class Shark: AquariumFish() {
override val color = "gray"
}
class Plecostomus: AquariumFish() {
override val color = "gold"
}
- 在**main.kt**中,创建一个
makeFish()
函数来测试你的类。实例化一个Shark
和一个Plecostomus
,然后打印每个鱼的颜色。 - 删除你在
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()
}
- 运行程序并观察输出。
⇒ Shark: gray Plecostomus: gold
下图表示Shark
类和Plecostomus
类,它们是抽象类AquariumFish
的子类。
步骤 2. 创建一个接口
- 在**AquariumFish.kt**中,创建一个名为
FishAction
的接口,其中包含一个方法eat()
。
interface FishAction {
fun eat()
}
- 将
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")
}
}
- 在
makeFish()
函数中,通过调用eat()
让创建的每条鱼吃一些东西。
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
shark.eat()
println("Plecostomus: ${pleco.color}")
pleco.eat()
}
- 运行程序并观察输出。
⇒ 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")
}
8. 任务:使用接口委托
前面的任务介绍了抽象类、接口和组合的概念。接口委托是一种高级技术,其中接口的方法由一个辅助(或委托)对象实现,然后由类使用。当你在一系列不相关的类中使用接口时,此技术非常有用:你将所需接口功能添加到一个单独的辅助类中,并且每个类都使用辅助类的实例来实现该功能。
在此任务中,你使用接口委托向类添加功能。
步骤 1:创建一个新的接口
- 在**AquariumFish.kt**中,删除
AquariumFish
类。Plecostomus
和Shark
将不再继承AquariumFish
类,而是实现鱼的行为和颜色的接口。 - 创建一个新的接口
FishColor
,它将颜色定义为字符串。
interface FishColor {
val color: String
}
- 更改
Plecostomus
以实现两个接口:FishAction
和FishColor
。你需要重写来自FishColor
的color
和来自FishAction
的eat()
。
class Plecostomus: FishAction, FishColor {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
- 更改你的
Shark
类,使其也实现两个接口FishAction
和FishColor
,而不是从AquariumFish
继承。
class Shark: FishAction, FishColor {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
- 完成后的代码应如下所示
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 中实现单例的方式。
- 在**AquariumFish.kt**中,为
GoldColor
创建一个对象。重写颜色。
object GoldColor : FishColor {
override val color = "gold"
}
步骤 3:为 FishColor 添加接口委托
现在你已准备好使用接口委托。
- 在**AquariumFish.kt**中,从
Plecostomus
中删除color
的重写。 - 更改
Plecostomus
类以从GoldColor
获取其颜色。你通过在类声明中添加by GoldColor
来实现此操作,从而创建委托。这意味着,它不是实现FishColor
,而是使用GoldColor
提供的实现。因此,每次访问color
时,它都会委托给GoldColor
。
class Plecostomus: FishAction, FishColor by GoldColor {
override fun eat() {
println("eat algae")
}
}
对于当前的类,所有 Plecos 都是金色的,但这些鱼实际上有多种颜色。你可以通过添加一个带有颜色的构造函数参数来解决此问题,其中GoldColor
作为Plecostomus
的默认颜色。
- 更改
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
使用接口委托。
- 在**AquariumFish.kt**中,创建一个
PrintingFishAction
类,该类实现FishAction
,它接收一个String
类型的food
,然后打印鱼吃的东西。
class PrintingFishAction(val food: String) : FishAction {
override fun eat() {
println(food)
}
}
- 在
Plecostomus
类中,删除重写函数eat()
,因为你将用委托替换它。 - 在
Plecostomus
的声明中,将FishAction
委托给PrintingFishAction
,并传递"eat algae"
。 - 通过所有这些委托,
Plecostomus
类的主体中没有代码,因此删除{}
,因为所有重写都由接口委托处理。
class Plecostomus (fishColor: FishColor = GoldColor):
FishAction by PrintingFishAction("eat algae"),
FishColor by fishColor
下图表示Shark
和Plecostomus
类,它们都包含PrintingFishAction
和FishColor
接口,但将实现委托给它们。
接口委托功能强大,并且在其他语言中可能使用抽象类时,你通常应该考虑如何使用它。它允许你使用组合来插入行为,而不是需要许多专门化方式的子类。
9. 任务:创建数据类
数据类类似于某些其他语言中的struct
——它主要用于保存一些数据——但是数据类对象仍然是对象。Kotlin 数据类对象具有一些额外的优势,例如打印和复制的实用程序。在此任务中,你将创建一个简单的数据类,并了解 Kotlin 为数据类提供的支持。
步骤 1:创建数据类
- 在**example.myapp**包下添加一个名为
decor
的新包以保存新代码。右键单击**Project**窗格中的**example.myapp**,然后选择**File > New > Package**。 - 在包中,创建一个名为
Decoration
的新类。
package example.myapp.decor
class Decoration {
}
- 要将
Decoration
设为数据类,请在类声明前添加关键字data
。 - 添加一个名为
rocks
的String
属性以向类提供一些数据。
data class Decoration(val rocks: String) {
}
- 在文件外部,类之外,添加一个
makeDecorations()
函数来创建一个Decoration
实例并打印,该实例的装饰材料为"granite"
。
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
}
- 添加一个
main()
函数来调用makeDecorations()
,并运行你的程序。注意由于这是一个数据类,所以会生成有意义的输出。
⇒ Decoration(rocks=granite)
- 在
makeDecorations()
中,再实例化两个Decoration
对象,它们都为 "slate",并打印它们。
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
val decoration2 = Decoration("slate")
println(decoration2)
val decoration3 = Decoration("slate")
println(decoration3)
}
- 在
makeDecorations()
中,添加一个打印语句,打印decoration1
与decoration2
比较的结果,以及decoration3
与decoration2
比较的结果。使用数据类提供的 equals() 方法。
println (decoration1.equals(decoration2))
println (decoration3.equals(decoration2))
- 运行你的代码。
⇒ 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
来声明枚举。基本的枚举声明只需要一个名称列表,但你也可以为每个名称定义一个或多个关联的字段。
- 在 Decoration.kt 中,尝试一个枚举示例。
enum class Color(val rgb: Int) {
RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}
枚举有点像单例——只能有一个,并且枚举中每个值也只有一个。例如,只能有一个 Color.RED
,一个 Color.GREEN
和一个 Color.BLUE
。在本例中,RGB 值被分配给 rgb
属性以表示颜色分量。你还可以使用 ordinal
属性获取枚举的序数值,并使用 name
属性获取其名称。
- 尝试另一个枚举示例。
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 会在静态地知道所有子类。也就是说,在编译时,编译器会看到所有类和子类,并知道这就是全部,因此编译器可以为你执行额外的检查。
- 在 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
,但你可以使用修饰符将其可见性更改为internal
、private
或protected
。 - 要创建子类,父类必须标记为
open
。 - 要在子类中覆盖方法和属性,方法和属性必须在父类中标记为
open
。 - 密封类只能在其定义的同一文件中进行子类化。通过在声明前加上
sealed
来创建密封类。
数据类、单例和枚举
- 通过在声明前加上
data
来创建数据类。 - 解构是将
data
对象的属性分配给单独变量的简写方式。 - 通过使用
object
而不是class
来创建单例类。 - 使用
enum class
定义枚举。
抽象类、接口和委托
- 抽象类和接口是两种在类之间共享通用行为的方式。
- 抽象类定义属性和行为,但将实现留给子类。
- 接口定义行为,并且可以为部分或全部行为提供默认实现。
- 当你使用接口来组合类时,类的功能会通过它包含的类实例来扩展。
- 接口委托使用组合,但也将实现委托给接口类。
- 组合是使用接口委托向类添加功能的强大方法。一般来说,组合是首选,但从抽象类继承更适合某些问题。
12. 了解更多
Kotlin 文档
如果你想了解更多有关本课程中任何主题的信息,或者如果你遇到困难,https://kotlinlang.org 是你最好的起点。
- Kotlin 编码约定
- Kotlin 习惯用法
- 类和继承
- 构造函数
- 工厂函数
- 属性和字段
- 可见性修饰符
- 抽象类
- 接口
- 委托
- 数据类
- 相等性
- 解构
- 对象声明
- 枚举类
- 密封类
- 使用 Kotlin 密封类处理可选错误
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 集训营:欢迎参加课程。”