1. 术语
以下编程术语您应该已经熟悉
- 类是对象的蓝图。例如,一个
Aquarium
类是用于创建Aquarium
对象的蓝图。 - 对象是类的实例;一个水族箱对象是在内存中存在的实际
Aquarium
。 - 属性是类的特征,例如
Aquarium
的长度、宽度和高度。 - 方法,也称为成员函数,是类的功能。方法是您可以对对象“执行”的操作。例如,您可以
fillWithWater()
一个Aquarium
对象。 - 接口是类可以实现的规范。例如,清洁对于水族箱以外的对象也很常见,并且不同对象的清洁通常以类似的方式进行。因此,您可以有一个名为
Clean
的接口,该接口定义了一个clean()
方法。Aquarium
类可以实现Clean
接口,以用软海绵清洁水族箱。 - 包是用于对相关代码进行分组以保持其组织性或创建代码库的一种方式。创建包后,您可以使用
import
直接引用该包中的类。
2. 创建一个类
在此任务中,您将创建一个新包以及一个包含一些属性和方法的类。
步骤 1:创建包
包可以帮助您整理代码。
- 在**项目**窗格中,在**Hello Kotlin**项目下,右键单击**src > main > kotlin**文件夹。
- 选择**新建 > 包**,并将其命名为
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
类的实例,类似于在其他语言中使用新的关键字
。 - 定义一个
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
3. 添加类构造函数
在此任务中,您将为类创建构造函数,并继续处理属性。
步骤 1:创建构造函数
在此步骤中,您将向在第一个任务中创建的Aquarium
类添加一个构造函数。在前面的示例中,每个Aquarium
实例都使用相同的尺寸创建。创建后,您可以通过设置属性来更改尺寸,但从一开始就创建正确的尺寸会更简单。
在 Java 等一些编程语言中,构造函数是通过在类中创建与类同名的函数来定义的。在 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
块以打印对象正在初始化,以及第二个init
块以打印以升为单位的体积。请注意,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} liters")
}
...
}
- 运行程序并观察输出。
aquarium initializing
Volume: 80 liters
Width: 20 cm Length: 100 cm Height: 40 cm
aquarium initializing
Volume: 100 liters
Width: 25 cm Length: 100 cm Height: 40 cm
aquarium initializing
Volume: 77 liters
Width: 20 cm Length: 110 cm Height: 35 cm
aquarium initializing
Volume: 96 liters
Width: 25 cm Length: 110 cm Height: 35 cm
请注意,init
块按其在类定义中出现的顺序执行,并且在调用构造函数时会执行所有init
块。
步骤 3:了解辅助构造函数
在此步骤中,您将了解辅助构造函数并将其添加到您的类中。除了可以包含一个或多个init
块的主构造函数外,Kotlin 类还可以包含一个或多个辅助构造函数。此功能允许构造函数重载,即具有不同参数的构造函数。
- 在
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
}
- 在辅助构造函数内,保持长度和宽度(在主构造函数中设置)相同,并计算使水箱达到给定体积所需的高度。
// 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} liters")
}
- 运行程序并观察输出。
⇒ aquarium initializing Volume: 80 liters Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 liters
请注意,体积打印了两次,一次是在执行辅助构造函数之前由主构造函数中的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 liter
- 删除打印体积的
init
代码块。 - 删除
buildAquarium()
中打印体积的代码。 - 在
printSize()
方法中,添加一行代码来打印体积。
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm "
)
// 1 liter = 1000 cm^3
println("Volume: $volume liters")
}
- 运行程序并观察输出。
⇒ aquarium initializing Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 liters
尺寸和体积与之前相同,但体积仅在对象由主构造函数和次构造函数完全初始化后打印一次。
步骤 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()
中,添加代码将 Aquarium 的体积设置为 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 liters
Width: 20 cm Length: 100 cm Height: 35 cm
Volume: 70 liters
4. 了解可见性修饰符
到目前为止,代码中还没有可见性修饰符,例如 public
或 private
。这是因为默认情况下,Kotlin 中的所有内容都是公共的,这意味着所有内容都可以在任何地方访问,包括类、方法、属性和成员变量。
在 Kotlin 中,类、对象、接口、构造函数、函数、属性及其 setter 可以具有可见性修饰符
private
表示它仅在该类(或如果您使用函数,则为源文件)中可见。protected
与private
相同,但它也对任何子类可见。internal
表示它仅在该模块中可见。模块 是一组一起编译的 Kotlin 文件,例如库、客户端或应用程序、IntelliJ 项目中的服务器应用程序。请注意,此处“模块”的用法与 Java 9 中引入的 Java 模块无关。public
表示在类外部可见。默认情况下,所有内容都是公共的,包括类的变量和方法。
有关更多信息,请参阅 Kotlin 文档中的 可见性修饰符。
成员变量
类中的属性或成员变量默认情况下为 public
。如果使用 var
定义它们,则它们是可变的,即可读和可写。如果使用 val
定义它们,则它们在初始化后是只读的。
如果希望代码可以读取或写入属性,但外部代码只能读取,则可以将属性及其 getter 保持为 public,并将 setter 声明为 private,如下所示。
var volume: Int
get() = width * height * length / 1000
private set(value) {
height = (value * 1000) / (width * length)
}
5. 了解子类和继承
在此任务中,您将学习 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)
}
- 添加一个值为
"rectangle"
的 openshape
属性。
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 liters Water: $water liters (${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 liters Water: 22.5 liters (90.0% full)
步骤 2:创建子类
- 创建
Aquarium
的一个子类,名为TowerTank
,它实现圆柱形水箱而不是矩形水箱。您可以将TowerTank
添加到Aquarium
下方,因为您可以在与Aquarium
类相同的文件中添加另一个类。 - 在
TowerTank
中,覆盖在构造函数中定义的height
属性。要覆盖属性,请在子类中使用override
关键字。
- 使
TowerTank
的构造函数接受一个diameter
。在调用Aquarium
超类的构造函数时,对length
和width
都使用diameter
。
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
- 覆盖体积属性以计算圆柱体的体积。圆柱体的公式是 π 乘以半径的平方乘以高度。请注意,IntelliJ 可能会将 PI 标记为未定义。您需要在
Main.kt
的顶部从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 liters Water: 22.5 liters (90.0% full) aquarium initializing cylinder Width: 25 cm Length: 25 cm Height: 40 cm Volume: 18 liters Water: 14.4 l (80.0% full)
6. 比较抽象类和接口
有时您希望定义一些相关类之间共享的通用行为或属性。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 = "grey"
}
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: grey Plecostomus: gold
下图表示我们应用程序的类层次结构。Shark
类和 Plecostomus
类都子类化抽象类 AquariumFish
。
步骤 2. 创建接口
- 在 AquariumFish.kt 中,创建一个名为
FishAction
的接口,其中包含一个方法eat()
。
interface FishAction {
fun eat()
}
- 将
FishAction
添加到每个子类中,并通过打印鱼的行为来实现eat()
。
class Shark: AquariumFish(), FishAction {
override val color = "grey"
override fun eat() {
println("hunt and eat fish")
}
}
class Plecostomus: AquariumFish(), FishAction {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
- 在 Main.kt 中的
makeFish()
函数中,让您创建的每条鱼通过调用eat()
来吃东西。
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
shark.eat()
println("Plecostomus: ${pleco.color}")
pleco.eat()
}
- 运行程序并观察输出。
⇒ Shark: grey hunt and eat fish Plecostomus: gold eat algae
下图表示 Shark
类和 Plecostomus
类,它们都实现了 FishAction
接口。
何时使用抽象类与接口
以上示例很简单,但是当您有很多相互关联的类时,抽象类和接口可以帮助您使设计更简洁、更有条理且更易于维护。
如上所述,抽象类可以有构造函数,而接口不能,但在其他方面它们非常相似。那么,您应该何时使用它们呢?
当您使用接口设计类时,类的功能通过它实现的接口中的方法扩展。使用接口中定义的特征将使代码比从抽象类继承更容易重用和理解。此外,您可以在一个类中实现多个接口,但只能从一个类继承。经验法则是尽可能优先考虑组合(即接口和实例引用)而不是子类化。
- 任何时候,如果无法完成一个类,都可以使用抽象类。例如,回到
AquariumFish
类,可以使所有AquariumFish
实现FishAction
,并为eat
提供默认实现,同时将color
设为抽象的,因为鱼类并没有真正的默认颜色。
interface FishAction {
fun eat()
}
abstract class AquariumFish : FishAction {
abstract val color: String
override fun eat() = println("yum")
}
7. 使用接口委托
前面的任务介绍了抽象类和接口。接口委托 是一种高级设计技巧,其中接口的方法由一个辅助(或委托)对象实现,然后由类使用。当在一系列不相关的类中使用接口时,此技术非常有用。您可以在单独的辅助类中实现所需的接口功能。然后,每个不相关的类都使用该辅助类的实例来获取功能。
在本任务中,您将使用接口委托为类添加功能。
步骤 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 = "grey"
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 = "grey"
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")
}
}
按照目前的类,所有 Plecostomus
实例都将是“金色”。但这些鱼实际上有许多颜色。您可以通过添加一个颜色构造函数参数来解决此问题,并将 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
创建了类似的设计,则下图将表示 Shark
和 Plecostomus
类。它们都由 PrintingFishAction
和 FishColor
接口组成,但将实现委托给它们。
接口委托功能强大,并且通常在其他语言中可能使用抽象类时,应考虑如何使用它。它允许您使用组合来插入行为,而不是需要大量子类,每个子类都以不同的方式进行专门化。
组合通常会导致更好的 封装、更低的 耦合(相互依赖)、更简洁的接口和更易用的代码。出于这些原因,使用接口进行组合是首选设计。另一方面,从抽象类继承往往是某些问题的自然选择。因此,您应该优先选择组合,但是当继承有意义时,Kotlin 也允许您这样做!
8. 创建数据类
一个 data
类类似于某些其他语言中的 struct
。它主要用于保存一些数据。Kotlin data
类具有一些额外的好处,例如打印和复制的实用程序。在本任务中,您将创建一个简单的数据类,并了解 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
9. 了解单例和枚举
在本任务中,您将了解 Kotlin 中的一些特殊用途的类,包括以下内容
- 单例类
- 伴随对象
- 枚举
步骤 1:回顾单例类
回顾前面 GoldColor
类的示例。
object GoldColor : FishColor {
override val color = "gold"
}
因为每个 GoldColor
实例都做相同的事情,所以将其声明为 object
而不是 class
以使其成为单例。它只能有一个实例。
步骤 2:创建一个枚举
Kotlin 也支持枚举,枚举是一组命名值或常量。在 Kotlin 中,枚举是特殊的类类型,允许您通过名称引用值,就像其他语言一样。它们可以提高代码的可读性。 enum
中的每个常量都是一个对象。通过在声明前加上关键字 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
属性获取其名称。
- 在 REPL 中尝试另一个枚举示例。
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
10. 总结
本课涵盖了很多内容。虽然其中许多内容在其他面向对象编程语言中应该很熟悉,但 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
。
数据类、单例和枚举
- 通过在声明前加上
data
来创建数据类。 - 解构是将
data
对象的属性分配给单独变量的简写方式。 - 通过使用
object
而不是class
来创建单例类。 - 使用
enum class
定义枚举。
抽象类、接口和委托
- 抽象类和接口是两种在类之间共享公共行为的方式。
- 抽象类定义属性和行为,但将实现留给子类。
- 接口定义行为,并可能为部分或全部行为提供默认实现。
- 当您使用接口来组合类时,类的功能将通过它包含的类实例来扩展。
- 接口委托通过将实现委托给接口类来使用组合。
- 组合是使用接口委托向类添加功能的强大方法。通常情况下,组合更受欢迎,但对于某些问题,从抽象类继承更合适。