在 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 类的实例,类似于在其他语言中使用 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 

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

  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 块按其在类定义中出现的顺序执行,并且在调用构造函数时会执行所有这些块。

步骤 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. 覆盖体积属性以计算圆柱体。圆柱体的公式是 pi 乘以半径的平方乘以高度。请注意,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() 函数以创建和打印一个带有 "granite"Decoration 实例。
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. 添加一个 main() 函数来调用 makeDecorations(),并运行您的程序。请注意由于这是一个数据类而创建的合理的输出。
⇒ Decoration(rocks=granite)
  1. makeDecorations() 中,实例化另外两个都是“slate”的 Decoration 对象并打印它们。
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. 了解更多