在 Kotlin 中使用类和对象

1. 术语

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

  • 是对象的蓝图。例如,一个Aquarium类是用于创建Aquarium对象的蓝图。
  • 对象是类的实例;一个水族箱对象是在内存中存在的实际Aquarium
  • 属性是类的特征,例如Aquarium的长度、宽度和高度。
  • 方法,也称为成员函数,是类的功能。方法是您可以对对象“执行”的操作。例如,您可以fillWithWater()一个Aquarium对象。
  • 接口是类可以实现的规范。例如,清洁对于水族箱以外的对象也很常见,并且不同对象的清洁通常以类似的方式进行。因此,您可以有一个名为Clean的接口,该接口定义了一个clean()方法。Aquarium类可以实现Clean接口,以用软海绵清洁水族箱。
  • 是用于对相关代码进行分组以保持其组织性或创建代码库的一种方式。创建包后,您可以使用import直接引用该包中的类。

2. 创建一个类

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

步骤 1:创建包

包可以帮助您整理代码。

  1. 在**项目**窗格中,在**Hello Kotlin**项目下,右键单击**src > main > kotlin**文件夹。
  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类的实例,类似于在其他语言中使用新的关键字
  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 

3. 添加类构造函数

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

步骤 1:创建构造函数

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

在 Java 等一些编程语言中,构造函数是通过在类中创建与类同名的函数来定义的。在 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块以打印对象正在初始化,以及第二个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")
    }
    ...
}
  1. 运行程序并观察输出。
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 类还可以包含一个或多个辅助构造函数。此功能允许构造函数重载,即具有不同参数的构造函数。

  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. 在辅助构造函数内,保持长度和宽度(在主构造函数中设置)相同,并计算使水箱达到给定体积所需的高度。
    // 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} liters")
}
  1. 运行程序并观察输出。
⇒ 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 需要返回计算出的值,您可以使用单行函数来实现 该函数紧跟在属性名称和类型之后

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

尺寸和体积与之前相同,但体积仅在对象由主构造函数和次构造函数完全初始化后打印一次。

步骤 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() 中,添加代码将 Aquarium 的体积设置为 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 liters
Width: 20 cm Length: 100 cm Height: 35 cm 
Volume: 70 liters

4. 了解可见性修饰符

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

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

  • private 表示它仅在该类(或如果您使用函数,则为源文件)中可见。
  • protectedprivate 相同,但它也对任何子类可见。
  • 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,以便您可以在下一步中覆盖它。

  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. 添加一个值为 "rectangle" 的 open shape 属性。
   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 liters Water: $water liters (${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 liters Water: 22.5 liters (90.0% full)

步骤 2:创建子类

  1. 创建 Aquarium 的一个子类,名为 TowerTank,它实现圆柱形水箱而不是矩形水箱。您可以将 TowerTank 添加到 Aquarium 下方,因为您可以在与 Aquarium 类相同的文件中添加另一个类。
  2. TowerTank 中,覆盖在构造函数中定义的 height 属性。要覆盖属性,请在子类中使用 override 关键字。
  1. 使 TowerTank 的构造函数接受一个 diameter。在调用 Aquarium 超类的构造函数时,对 lengthwidth 都使用 diameter
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
  1. 覆盖体积属性以计算圆柱体的体积。圆柱体的公式是 π 乘以半径的平方乘以高度。请注意,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()
    }
  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 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. 创建抽象类

  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 = "grey"
}

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: grey 
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 = "grey"
    override fun eat() {
        println("hunt and eat fish")
    }
}

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

下图表示 Shark 类和 Plecostomus 类,它们都实现了 FishAction 接口。

e4b747e0303bcdaa.png

何时使用抽象类与接口

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

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

当您使用接口设计类时,类的功能通过它实现的接口中的方法扩展。使用接口中定义的特征将使代码比从抽象类继承更容易重用和理解。此外,您可以在一个类中实现多个接口,但只能从一个类继承。经验法则是尽可能优先考虑组合(即接口和实例引用)而不是子类化。

  • 任何时候,如果无法完成一个类,都可以使用抽象类。例如,回到 AquariumFish 类,可以使所有 AquariumFish 实现 FishAction,并为 eat 提供默认实现,同时将 color 设为抽象的,因为鱼类并没有真正的默认颜色。
interface FishAction  {
    fun eat()
}

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

7. 使用接口委托

前面的任务介绍了抽象类和接口。接口委托 是一种高级设计技巧,其中接口的方法由一个辅助(或委托)对象实现,然后由类使用。当在一系列不相关的类中使用接口时,此技术非常有用。您可以在单独的辅助类中实现所需的接口功能。然后,每个不相关的类都使用该辅助类的实例来获取功能。

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

步骤 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 = "grey"
    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 = "grey"
    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")
   }
}

按照目前的类,所有 Plecostomus 实例都将是“金色”。但这些鱼实际上有许多颜色。您可以通过添加一个颜色构造函数参数来解决此问题,并将 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

如果您为 Shark 创建了类似的设计,则下图将表示 SharkPlecostomus 类。它们都由 PrintingFishActionFishColor 接口组成,但将实现委托给它们。

a7556eed5ec884a3.png

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

组合通常会导致更好的 封装、更低的 耦合(相互依赖)、更简洁的接口和更易用的代码。出于这些原因,使用接口进行组合是首选设计。另一方面,从抽象类继承往往是某些问题的自然选择。因此,您应该优先选择组合,但是当继承有意义时,Kotlin 也允许您这样做!

8. 创建数据类

一个 data 类类似于某些其他语言中的 struct。它主要用于保存一些数据。Kotlin data 类具有一些额外的好处,例如打印和复制的实用程序。在本任务中,您将创建一个简单的数据类,并了解 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

9. 了解单例和枚举

在本任务中,您将了解 Kotlin 中的一些特殊用途的类,包括以下内容

  • 单例类
  • 伴随对象
  • 枚举

步骤 1:回顾单例类

回顾前面 GoldColor 类的示例。

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

因为每个 GoldColor 实例都做相同的事情,所以将其声明为 object 而不是 class 以使其成为单例。它只能有一个实例。

步骤 2:创建一个枚举

Kotlin 也支持枚举,枚举是一组命名值或常量。在 Kotlin 中,枚举是特殊的类类型,允许您通过名称引用值,就像其他语言一样。它们可以提高代码的可读性。 enum 中的每个常量都是一个对象。通过在声明前加上关键字 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. 在 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,但您可以使用修饰符将可见性更改为 internalprivateprotected
  • 要创建子类,父类必须标记为 open
  • 要在子类中覆盖方法和属性,方法和属性必须在父类中标记为 open

数据类、单例和枚举

  • 通过在声明前加上 data 来创建数据类。
  • 解构是将 data 对象的属性分配给单独变量的简写方式。
  • 通过使用 object 而不是 class 来创建单例类。
  • 使用 enum class 定义枚举。

抽象类、接口和委托

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

11. 了解更多