Kotlin 中的类和继承

1. 准备工作

前提条件

  • 熟悉如何使用 Kotlin Playground 修改 Kotlin 程序。
  • 了解本课程第 1 单元中介绍的 Kotlin 编程基本概念,尤其是 main() 程序、能够返回值的带参数的函数、变量、数据类型和操作,以及 if/else 语句。
  • 能够在 Kotlin 中定义类、通过该类创建对象实例以及访问其属性和方法。

学习内容

  • 创建一个利用继承来实现类层次结构的 Kotlin 程序。
  • 扩展类、替换其现有函数并添加新函数。
  • 为变量选择正确的可见性修饰符。

构建内容

  • 一个 Kotlin 程序,包含以类层次结构形式实现的不同类型的住房。

所需条件

2. 什么是类层次结构?

人们很自然地会将具有相似属性和行为的事物划分为组,甚至会在这些组之间建立某种类型的层次结构。例如,您可以有一个比较宽泛的类别(例如蔬菜),在该类别内,您可以设置更具体的类型(例如豆类)。在豆类中,您可以划分更加具体的类型,例如豌豆、黄豆、扁豆、鹰嘴豆和大豆。

这可以表示为层次结构,因为豆类包含或继承了蔬菜的所有属性(例如,它们属于植物,且可以食用)。同样地,豌豆、黄豆和扁豆均具有豆类的属性,同时也具有自己独特的属性。

我们来看看,如何用编程术语表示这种关系。如果您将 Vegetable 作为 Kotlin 中的一个类,您可以创建 Legume 作为 Vegetable 类的子级或子类。这意味着,Vegetable 类的所有属性和方法均会被 Legume 类继承(即,这些属性和方法同样可用于后者)。

您可以在类层次结构示意图中表示这种关系,如下所示。您可将 Vegetable 称为 Legume 类的父级或父类。

87e0a5eb0f85042d.png

您可以通过创建 Legume 的子类(例如 LentilChickpea)继续扩展类层次结构。这会使 Legume 既是 Vegetable 的子级或子类,同时也是 LentilChickpea 的父级或父类。Vegetable 是此层次结构的根类或顶级类(也称为基类)。

638655b960530d9.png

Android 类中的继承

正如您在之前的 Codelab 中所做的那样,虽然您可以在不使用类的情况下编写 Kotlin 代码,但 Android 的很多部分都是以类的形式向您提供的,包括 activity、视图和视图组。因此,理解类层次结构是 Android 应用开发的基础,并且有助于您利用 Android 框架提供的诸多功能。

例如,Android 中有一个 View 类,它表示屏幕上的一个矩形区域,并且负责绘制和事件处理。TextView 类是 View 类的子类,这意味着 TextView 会继承 View 类的所有属性和函数,并会添加特定的逻辑,以向用户显示文本。

c39a8aaa5b013de8.png

再往下一级,EditTextButton 类是 TextView 类的子级。它们会继承 TextViewView 类的所有属性和方法,并会添加自己的特定逻辑。例如,EditText 添加了自己的功能,能够修改屏幕上的文本。

您不必从 ViewTextView 类中复制所有逻辑,并将其粘贴到 EditText 类中,EditText 可以直接子类化 TextView 类,后者再子类化 View 类。然后,EditText 类中的代码便可专注于使此界面组件不同于其他视图的其他方面。

在 developer.android.com 网站上有关 Android 类的文档页面顶部,您可以看到类层次结构示意图。如果您在层次结构顶部看到 kotlin.Any,这是因为在 Kotlin 中,所有类都具有一个通用父类 Any。请点击此处了解详情。

1ce2b1646b8064ab.png

如您所见,学习利用类之间的继承关系,可使您的代码更易于编写、复用、阅读和测试。

3. 创建基类

住房的类层次结构

在此 Codelab 中,您将构建一个 Kotlin 程序,该程序会以具有建筑空间、楼层和住客的住房(人们的住处)为例,演示类层次结构的工作方式。

下面是您要构建的类层次结构的示意图。在根部,有一个 Dwelling,它指定了适用于所有住房的属性和函数,类似于蓝图。然后还有多个类,分别对应方形小木屋 (SquareCabin)、圆形小屋 (RoundHut) 和圆形塔楼 (RoundTower)(即多楼层的 RoundHut)。

de1387ca7fc26c81.png

您将实现的类:

  • Dwelling:一个表示非特定住房的基类,可存储所有住房通用的信息。
  • SquareCabin:用木材建成的方形小木屋,建筑平面为方形。
  • RoundHut:用稻草搭成的圆形小屋,建筑平面为圆形,它是 RoundTower 的父级。
  • RoundTower:用石头建成的多楼层圆形塔楼,建筑平面为圆形。

创建 Dwelling 抽象类

任何类都可以是类层次结构的基类或其他类的父类。

“抽象”类是一种无法实例化的类,因为它没有完全实现。您可以将其视为一个草图。草图包含关于某些事的想法和计划,但其中的信息通常并不足以完成构建。您可以使用草图(抽象类)来创建一个蓝图(类),然后基于后者构建实际的对象实例。

创建父类的一个常见好处是,能够包含其所有子类通用的属性和函数。如果属性的值及函数的实现情况未知,则可以将其作为抽象类。例如,Vegetables 中有许多所有蔬菜通用的属性,但您无法为某种非特定的蔬菜创建实例,因为您不知道关于它的某些信息,例如其形状或颜色。因此,Vegetable 是一个抽象类,每种蔬菜的具体细节交由子类来确定。

抽象类的声明以 abstract 关键字开头。

DwellingVegetable 一样,也会是一个抽象类。它将包含很多类住房通用的属性和函数,但属性的确切值和函数的实现细节未知。

  1. 访问 Kotlin Playground,网址为:https://developer.android.com/training/kotlinplayground
  2. 在编辑器中,删除 main() 函数中的 println("Hello, world!")
  3. 然后,在 main() 函数下方添加以下代码,以创建一个名为 Dwellingabstract 类。
abstract class Dwelling(){
}

添加有关建筑材料的属性

在此 Dwelling 类中,您可以定义适用于所有住房的属性,即使对于不同住房来说,这些属性的值可能会有所不同。所有住房都是由某种建筑材料建造而成的。

  1. Dwelling 中,创建一个类型为 StringbuildingMaterial 变量来表示建筑材料。由于建筑材料不会变化,可以使用 val 使其成为不可变变量。
val buildingMaterial: String
  1. 运行程序,系统会显示以下错误消息。
Property must be initialized or be abstract

buildingMaterial 属性没有值。实际上,您不能为其提供值,因为非特定建筑并非由任何特定建筑材料建造而成。因此,如错误消息所示,您可以添加 abstract 关键字作为 buildingMaterial 声明的前缀,以表明该属性不会在此处进行定义。

  1. abstract 关键字添加到变量定义中。
abstract val buildingMaterial: String
  1. 运行您的代码,虽然它不会执行任何操作,但现在能顺利编译,不会出现任何错误。
  2. main() 函数中构建一个 Dwelling 实例,然后运行代码。
val dwelling = Dwelling()
  1. 您将收到一条错误消息,因为您无法创建 Dwelling 抽象类的实例。
Cannot create an instance of an abstract class
  1. 删除此错误代码。

到目前为止,您已完成的代码如下所示:

abstract class Dwelling(){
    abstract val buildingMaterial: String
}

添加有关可容纳人数的属性

住房的另一个属性是可容纳人数,即屋内可以住多少人。

所有住房的可容纳人数都不会变。但是,不能在 Dwelling 父类中设置可容纳人数,而应在特定类型住房对应的子类中进行定义。

  1. Dwelling 中,添加一个名为 capacityabstract 整数 val
abstract val capacity: Int

添加有关住客人数的私有属性

所有住房都有一定数量的 residents(即住在该住房里的人数,该值可能小于或等于 capacity),因此应在 Dwelling 父类中定义 residents 属性,以便所有子类继承和使用。

  1. 您可以将 residents 作为传递给 Dwelling 类构造函数的参数。residents 属性是一个 var,因为住客人数在实例创建后可能会变化。
abstract class Dwelling(private var residents: Int) {

请注意,residents 属性使用 private 关键字进行了标记。Private 是 Kotlin 中的可见性修饰符,表示 residents 属性仅对此类可见(并且只能在此类中使用)。它不能从程序中的其他地方访问。您可以使用 private 关键字标记属性或方法。否则,如果不指定可见性修饰符,相应属性和方法会默认为 public,可以从程序的其他部分访问。由于住房的住客人数通常是隐私信息(相对于建筑材料或可容纳人数方面的信息),将其标记为 private 是明智之举。

定义好住房的 capacity 和当前 residents 人数后,您可以创建一个函数 hasRoom() 来确定该住房能否容纳其他住客。您可以在 Dwelling 类中定义和实现 hasRoom() 函数,因为用于计算能否容纳其他住客的公式对于所有住房都一样。如果 residents 人数小于 capacity,则意味着相应 Dwelling 能容纳其他住客,函数应按这种比较方法返回 truefalse

  1. hasRoom() 函数添加到 Dwelling 类。
fun hasRoom(): Boolean {
    return residents < capacity
}
  1. 您可以运行此代码,系统应该不会显示错误。该代码尚未执行任何可见操作。

完成后的代码应如下所示:

abstract class Dwelling(private var residents: Int) {

   abstract val buildingMaterial: String
   abstract val capacity: Int

   fun hasRoom(): Boolean {
       return residents < capacity
   }
}

4. 创建子类

创建 SquareCabin 子类

  1. Dwelling 类下,创建一个名为 SquareCabin 的类。
class SquareCabin
  1. 接下来,您需要指明 SquareCabinDwelling 相关。在您的代码中,您需要表明 SquareCabin 扩展自 Dwelling(或者是 Dwelling) 的子类),因为 SquareCabin 将会提供 Dwelling 抽象部分的实现。

通过在 SquareCabin 类名称后添加一个冒号 (:),后跟用于初始化父级 Dwelling 类的调用,表明此继承关系。不要忘记在 Dwelling 类名称后添加圆括号。

class SquareCabin : Dwelling()
  1. 如果是从父类扩展,您必须传入父类所需的参数。Dwelling 需要输入 residents 人数。您可以传入固定的住客人数,例如 3
class SquareCabin : Dwelling(3)

不过,您希望您的程序能够更加灵活,并且允许 SquareCabins 拥有不同的住客人数。因此,在 SquareCabin 类定义中将 residents 作为一个参数。不要将 residents 声明为 val,,因为您是在重复使用已在父类 Dwelling 中声明的属性。

class SquareCabin(residents: Int) : Dwelling(residents)
  1. 运行您的代码。
  2. 这会导致出现错误。我们来看一下:
Class 'SquareCabin' is not abstract and does not implement abstract base class member public abstract val buildingMaterial: String defined in Dwelling

如果您声明抽象函数和变量,就表示您承诺稍后将为它们提供值和实现。对于变量,这意味着该抽象类的任何子类都需要为其提供值。对于函数,这意味着任何子类都需要实现函数主体。

Dwelling 类中,您定义了一个 abstract 变量 buildingMaterialSquareCabinDwelling 的子类,因此它必须为 buildingMaterial 提供值。使用 override 关键字来表明此属性是在父类中定义的,并且在此类中将被替换掉。

  1. SquareCabin 类中,override buildingMaterial 属性并为其分配值 "Wood"
  2. 针对 capacity 执行相同的操作,并表明一个 SquareCabin 中可以住 6 人。
class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

完成后的代码应如下所示。

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

如需测试代码,请在程序中创建 SquareCabin 实例。

使用 SquareCabin

  1. DwellingSquareCabin 类定义前插入一个空 main() 函数。
fun main() {

}

abstract class Dwelling(private var residents: Int) {
    ...
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    ...
}
  1. main() 函数中,创建一个名为 squareCabin 且住客人数为 6 的 SquareCabin 实例。针对建筑材料、可容纳人数和 hasRoom() 函数添加输出语句。
fun main() {
    val squareCabin = SquareCabin(6)

    println("\nSquare Cabin\n============")
    println("Capacity: ${squareCabin.capacity}")
    println("Material: ${squareCabin.buildingMaterial}")
    println("Has room? ${squareCabin.hasRoom()}")
}

请注意,hasRoom() 函数不是在 SquareCabin 类中定义的,而是在 Dwelling 类中定义的。由于 SquareCabinDwelling 类的子类,hasRoom() 函数是未经任务操作直接继承而来的。现在,可以在 SquareCabin 的所有实例上调用 hasRoom() 函数,如代码段 squareCabin.hasRoom() 所示。

  1. 运行代码,输出内容应如下所示:
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

您创建的 squareCabin 住客人数为 6,该值等于 capacity 的值,因此 hasRoom() 返回 false。您可以尝试使用较少人数的 residents 初始化 SquareCabin,当您再次运行程序时,hasRoom() 应该会返回 true

使用 with 简化代码

println() 语句中,每次引用 squareCabin 的属性或函数时,都必须反复输入 squareCabin.。这会形成重复,可能在您复制和粘贴输出语句时造成错误。

当您使用某个类的特定实例,并需要访问该实例的多个属性和函数时,您可以使用 with 语句表明“对此实例对象执行以下所有操作”。先输入关键字 with,再在圆括号内输入实例名称,然后再输入大括号并在其中指明您想要执行的操作。

with (instanceName) {
    // all operations to do with instanceName
}
  1. main() 函数中,将您的输出语句改为使用 with
  2. 删除输出语句中的“squareCabin.”。
with(squareCabin) {
    println("\nSquare Cabin\n============")
    println("Capacity: ${capacity}")
    println("Material: ${buildingMaterial}")
    println("Has room? ${hasRoom()}")
}
  1. 再次运行代码,以确保它能够正常运行而不会出现任何错误,并输出相同的结果。
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

完成的代码如下所示:

fun main() {
    val squareCabin = SquareCabin(6)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

创建 RoundHut 子类

  1. 按照与添加 SquareCabin 相同的方式,为 Dwelling 添加另一个子类 RoundHut
  2. 替换 buildingMaterial 并为其提供值 "Straw"
  3. 替换 capacity 并将其设置为 4。
class RoundHut(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Straw"
    override val capacity = 4
}
  1. main() 中,创建一个住客人数为 3 的 RoundHut 实例。
val roundHut = RoundHut(3)
  1. 添加以下代码以输出 roundHut 的相关信息。
with(roundHut) {
    println("\nRound Hut\n=========")
    println("Material: ${buildingMaterial}")
    println("Capacity: ${capacity}")
    println("Has room? ${hasRoom()}")
}
  1. 运行代码,整个程序的输出应如下所示:
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true

现在,您有了一个如下所示的类层次结构,其中 Dwelling 为根类,SquareCabinRoundHutDwelling 的子类。

c19084f4a83193a0.png

创建 RoundTower 子类

这个类层次结构中处于最终层级的类是圆形塔楼。您可以将圆形塔楼视为一个由石头建造而成的多楼层圆形小屋。因此,您可以将 RoundTower 作为 RoundHut 的子类。

  1. 创建一个 RoundTower 类,它是 RoundHut 的子类。将 residents 参数添加到 RoundTower 的构造函数中,然后将该参数传递给 RoundHut 父类的构造函数。
  2. buildingMaterial 替换为 "Stone"
  3. capacity 设置为 4
class RoundTower(residents: Int) : RoundHut(residents) {
    override val buildingMaterial = "Stone"
    override val capacity = 4
}
  1. 运行此代码,您会收到一条错误消息。
This type is final, so it cannot be inherited from

此错误意味着,RoundHut 类不能被子类化(或被继承)。默认情况下,在 Kotlin 中,类是最终层级,无法被子类化。您只能从 abstract 类或标记有 open 关键字的类继承。因此,您需要使用 open 关键字标记 RoundHut 类,使其能够被继承。

  1. RoundHut 声明的开头添加 open 关键字。
open class RoundHut(residents: Int) : Dwelling(residents) {
   override val buildingMaterial = "Straw"
   override val capacity = 4
}
  1. main() 中,创建一个 roundTower 实例并输出其相关信息。
 val roundTower = RoundTower(4)
with(roundTower) {
    println("\nRound Tower\n==========")
    println("Material: ${buildingMaterial}")
    println("Capacity: ${capacity}")
    println("Has room? ${hasRoom()}")
}

以下是完整代码。

fun main() {
    val squareCabin = SquareCabin(6)
    val roundHut = RoundHut(3)
    val roundTower = RoundTower(4)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

open class RoundHut(residents: Int) : Dwelling(residents) {
   override val buildingMaterial = "Straw"
   override val capacity = 4
}

class RoundTower(residents: Int) : RoundHut(residents) {
    override val buildingMaterial = "Stone"
    override val capacity = 4
}
  1. 运行您的代码。它现在应该能够正常运行而不会出现任何错误,并且会生成以下输出。
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true

Round Tower
==========
Material: Stone
Capacity: 4
Has room? false

为 RoundTower 添加多个楼层

顾名思义,RoundHut 是单层建筑,而塔楼通常有多层(楼层)。

对于可容纳人数,塔楼的楼层越多,可容纳人数应该就越多。

您可以将 RoundTower 修改为多楼层,并根据楼层数量调整可容纳人数。

  1. 更新 RoundTower 构造函数,以接受另一个整数参数 val floors 作为楼层数量。将其置于 residents 之后。请注意,您无需将此参数传递给父级 RoundHut 构造函数,因为 floors 是在 RoundTower 中定义的,而 RoundHut 没有 floors
class RoundTower(
    residents: Int,
    val floors: Int) : RoundHut(residents) {

    ...
}
  1. 运行您的代码。在 main() 方法中创建 roundTower 时出现错误,因为您没有为 floors 参数提供数字。您可以添加缺少的参数。

或者,您也可以在 RoundTower 的类定义中为 floors 添加默认值,如下所示。然后,在没有 floors 值传递到构造函数的情况下,系统就可以使用默认值创建对象实例。

  1. 在您的代码中,在 floors 声明后添加 = 2,以为其分配默认值 2。
class RoundTower(
    residents: Int,
    val floors: Int = 2) : RoundHut(residents) {

    ...
}
  1. 运行您的代码。它应该能顺利编译,因为 RoundTower(4) 现在会使用 2 层楼的默认值创建 RoundTower 对象实例。
  2. RoundTower 类中,更新 capacity,用之前的值乘以楼层数。
override val capacity = 4 * floors
  1. 运行代码,请注意,RoundTower 的可容纳人数现在为 8 人(2 层)。

下面是完成后的代码。

fun main() {

    val squareCabin = SquareCabin(6)
    val roundHut = RoundHut(3)
    val roundTower = RoundTower(4)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

open class RoundHut(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Straw"
    override val capacity = 4
}

class RoundTower(
    residents: Int,
    val floors: Int = 2) : RoundHut(residents) {

    override val buildingMaterial = "Stone"
    override val capacity = 4 * floors
}

5. 修改层次结构中的类

计算建筑面积

在本练习中,您将学习如何在抽象类中声明抽象函数,然后在子类中实现其函数。

所有住房都具有建筑面积,但其计算方式因住房形状而有所不同。

在 Dwelling 类中定义 floorArea()

  1. 首先,向 Dwelling 类中添加一个 abstract floorArea() 函数。返回 Double。Double 与 StringInt 一样,是一种数据类型;它可用于浮点数(即带有小数点且后跟小数部分的数字,例如 5.8793)。
abstract fun floorArea(): Double

抽象类中定义的所有抽象方法都必须在其任意子类中实现。您需要先在子类中实现 floorArea(),然后才能运行代码。

针对 SquareCabin 实现 floorArea()

与实现 buildingMaterialcapacity 一样,由于您要实现的 abstract 函数是在父类中定义的,您需要使用 override 关键字。

  1. SquareCabin 类中,首先输入关键字 override,后跟 floorArea() 函数的实际实现,如下所示。
override fun floorArea(): Double {

}
  1. 返回计算出的建筑面积。矩形或方形的面积计算方法为,一边的长度乘以另一边的长度。函数主体为 return length * length
override fun floorArea(): Double {
    return length * length
}

在此类中,长度是一个变量,每个实例的长度各不相同,因此您可以将其添加为 SquareCabin 类的构造函数参数。

  1. 更改 SquareCabin 的类定义,以添加 Double 类型的 length 参数。将该属性声明为 val,因为建筑的长度不会变化。
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {

Dwelling 具有构造函数参数 residents,因此其所有子类都具有该参数。由于这是 Dwelling 构造函数中的第一个参数,所以最好也将其作为所有子类构造函数中的第一个参数,并在所有类定义中以相同的顺序排列参数。因此,在 residents 参数后插入新的 length 参数。

  1. main() 中,更新创建 squareCabin 实例的代码。将 50.0 作为 length 传入 SquareCabin 构造函数。
val squareCabin = SquareCabin(6, 50.0)
  1. squareCabinwith 语句中,添加针对建筑面积的输出语句。
println("Floor area: ${floorArea()}")

您的代码将不会运行,因为您还需要在 RoundHut 中实现 floorArea()

针对 RoundHut 实现 floorArea()

以同样的方式针对 RoundHut 实现建筑面积。RoundHut 也是 Dwelling 的直接子类,因此您需要使用 override 关键字。

圆形住房建筑面积的计算方法为,PI * 半径 * 半径。

PI 是一个数学值。它会在数学库中进行定义。库是一个预定义的函数和值集合,在程序外部定义,但可供程序使用。为了使用某个库函数或值,您需要告知编译器您将使用该函数或值。方法是将该函数或值导入到程序中。如需在程序中使用 PI,您需要导入 kotlin.math.PI

  1. 从 Kotlin 数学库导入 PI。将此导入语句放在文件的顶部,main() 之前。
import kotlin.math.PI
  1. 针对 RoundHut 实现 floorArea() 函数。
override fun floorArea(): Double {
    return PI * radius * radius
}

警告:如果您不导入 kotlin.math.PI,将会遇到错误,因此请在使用之前导入此库。或者,您可以写出 PI 的完全限定版本,如“kotlin.math.PI * 半径 * 半径”中所示,然后便无需添加导入语句。

  1. 更新 RoundHut 构造函数以传入 radius
open class RoundHut(
   residents: Int,
   val radius: Double) : Dwelling(residents) {
  1. main() 中,通过向 RoundHut 构造函数传入 radius 10.0,更新 roundHut 的初始化。
val roundHut = RoundHut(3, 10.0)
  1. roundHutwith 语句中添加输出语句。
println("Floor area: ${floorArea()}")

针对 RoundTower 实现 floorArea()

您的代码尚无法正常运行,它会失败并显示以下错误消息:

Error: No value passed for parameter 'radius'

RoundTower 中,为了使程序能够顺利编译,您不需要实现 floorArea(),因为它能够从 RoundHut 继承,但您需要更新 RoundTower 类定义,使其具有与其父级 RoundHut 相同的 radius 参数。

  1. 更改 RoundTower 构造函数,使其同样接受 radius。将 radius 放在 residents 之后、floors 之前。建议将具有默认值的变量列在末尾。请记得将 radius 传递给父类构造函数。
class RoundTower(
    residents: Int,
    radius: Double,
    val floors: Int = 2) : RoundHut(residents, radius) {
  1. main() 中更新初始化 roundTower 的代码。
val roundTower = RoundTower(4, 15.5)
  1. 添加一个调用 floorArea() 的输出语句。
println("Floor area: ${floorArea()}")
  1. 现在,您可以运行代码了!
  2. 请注意,RoundTower 的计算方法不正确,因为它继承自 RoundHut,并未考虑 floors 数量。
  3. RoundTower 中,override floorArea() 以便为其提供不同的实现,将面积乘以楼层数量。注意,您可以在抽象类 (Dwelling) 中定义一个函数,在子类 (RoundHut) 中实现它,然后在子类的子类 (RoundTower) 中再次替换它。这是一种两全其美的做法,您既可以继承自己需要的函数,又能替换掉自己不需要的函数。
override fun floorArea(): Double {
    return PI * radius * radius * floors
}

此代码能够正常运行,不过有一种方法可以避免反复使用父类 RoundHut 中已有的代码。您可以从父类 RoundHut 中调用 floorArea() 函数,后者会返回 PI * radius * radius。然后将该结果乘以 floors 数。

  1. RoundTower 中,将 floorArea() 更新为使用其父类中的 floorArea() 实现。使用 super 关键字,调用父级中定义的函数。
override fun floorArea(): Double {
    return super.floorArea() * floors
}
  1. 再次运行您的代码,RoundTower 将输出相应楼层数量对应的正确建筑面积。

下面是完成后的代码:

import kotlin.math.PI

fun main() {

    val squareCabin = SquareCabin(6, 50.0)
    val roundHut = RoundHut(3, 10.0)
    val roundTower = RoundTower(4, 15.5)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }
 }

abstract class Dwelling(private var residents: Int) {

    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
        return residents < capacity
}

    abstract fun floorArea(): Double
}

class SquareCabin(residents: Int,
    val length: Double) : Dwelling(residents) {

    override val buildingMaterial = "Wood"
    override val capacity = 6

    override fun floorArea(): Double {
       return length * length
    }
}

open class RoundHut(residents: Int,
    val radius: Double) : Dwelling(residents) {

    override val buildingMaterial = "Straw"
    override val capacity = 4

    override fun floorArea(): Double {
        return PI * radius * radius
    }
}

class RoundTower(residents: Int, radius: Double,
    val floors: Int = 2) : RoundHut(residents, radius) {

    override val buildingMaterial = "Stone"
    override val capacity = 4 * floors

    override fun floorArea(): Double {
        return super.floorArea() * floors
    }
}

输出应如下所示:

Square Cabin
============
Capacity: 6
Material: Wood
Has room? false
Floor area: 2500.0

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true
Floor area: 314.1592653589793

Round Tower
==========
Material: Stone
Capacity: 8
Has room? true
Floor area: 1509.5352700498956

允许新住客获得房间

使用 getRoom() 函数,使新住客能够获得房间,该函数会使住客人数以 1 为增量递增。由于此逻辑适用于所有住房,您可以在 Dwelling 中实现该函数,这样它就能够用于所有子类及其子级。太棒了!

注意:

  • 使用 if 语句,以便仅在未达到可容纳人数上限时添加住客。
  • 针对结果输出一条消息。
  • 您可以使用 residents++ 作为 residents = residents + 1 的简写形式,用于向 residents 变量加 1。
  1. Dwelling 类中实现 getRoom() 函数。
fun getRoom() {
    if (capacity > residents) {
        residents++
        println("You got a room!")
    } else {
        println("Sorry, at capacity and no rooms left.")
    }
}
  1. roundHutwith 语句块添加一些输出语句,以同时使用 getRoom()hasRoom() 观察变化。
println("Has room? ${hasRoom()}")
getRoom()
println("Has room? ${hasRoom()}")
getRoom()

这些输出语句的输出如下所示:

Has room? true
You got a room!
Has room? false
Sorry, at capacity and no rooms left.

如需了解详情,请参阅解决方案代码。

为圆形住房准备合适的地毯

假设您需要知道要为自己的 RoundHutRoundTower 准备多大边长的地毯。将相应函数放到 RoundHut 中,使其可用于所有圆形住房。

2e328a198a82c793.png

  1. 首先,从 kotlin.math 库导入 sqrt() 函数。
import kotlin.math.sqrt
  1. RoundHut 类中实现 calculateMaxCarpetLength() 函数。用于计算适合放入圆形中的方形地毯长度的公式为 sqrt(2) * radius。上图对此进行了说明。
fun calculateMaxCarpetLength(): Double {

    return sqrt(2.0) * radius
}

Double2.0 传入数学函数 sqrt(2.0),因为该函数的返回值类型是 Double,而不是 Integer

  1. 现在,calculateMaxCarpetLength() 方法可以在 RoundHutRoundTower 实例上调用了。在 main() 函数中,为 roundHutroundTower 添加输出语句。
println("Carpet Length: ${calculateMaxCarpetLength()}")

如需了解详情,请参阅解决方案代码。

恭喜!您已经创建了一个包含属性和函数的完整类层次结构,并学习了创建更多实用类所需的所有知识!

6. 解决方案代码

以下是此 Codelab 的完整解决方案代码,包括注释。

/**
* Program that implements classes for different kinds of dwellings.
* Shows how to:
* Create class hierarchy, variables and functions with inheritance,
* abstract class, overriding, and private vs. public variables.
*/

import kotlin.math.PI
import kotlin.math.sqrt

fun main() {
   val squareCabin = SquareCabin(6, 50.0)
   val roundHut = RoundHut(3, 10.0)
   val roundTower = RoundTower(4, 15.5)

   with(squareCabin) {
       println("\nSquare Cabin\n============")
       println("Capacity: ${capacity}")
       println("Material: ${buildingMaterial}")
       println("Floor area: ${floorArea()}")
   }

   with(roundHut) {
       println("\nRound Hut\n=========")
       println("Material: ${buildingMaterial}")
       println("Capacity: ${capacity}")
       println("Floor area: ${floorArea()}")
       println("Has room? ${hasRoom()}")
       getRoom()
       println("Has room? ${hasRoom()}")
       getRoom()
       println("Carpet size: ${calculateMaxCarpetLength()}")
   }

   with(roundTower) {
       println("\nRound Tower\n==========")
       println("Material: ${buildingMaterial}")
       println("Capacity: ${capacity}")
       println("Floor area: ${floorArea()}")
       println("Carpet Length: ${calculateMaxCarpetLength()}")
   }
}

/**
* Defines properties common to all dwellings.
* All dwellings have floorspace,
* but its calculation is specific to the subclass.
* Checking and getting a room are implemented here
* because they are the same for all Dwelling subclasses.
*
* @param residents Current number of residents
*/
abstract class Dwelling(private var residents: Int) {
   abstract val buildingMaterial: String
   abstract val capacity: Int

   /**
    * Calculates the floor area of the dwelling.
    * Implemented by subclasses where shape is determined.
    *
    * @return floor area
    */
   abstract fun floorArea(): Double

   /**
    * Checks whether there is room for another resident.
    *
    * @return true if room available, false otherwise
    */
   fun hasRoom(): Boolean {
       return residents < capacity
   }

   /**
    * Compares the capacity to the number of residents and
    * if capacity is larger than number of residents,
    * add resident by increasing the number of residents.
    * Print the result.
    */
   fun getRoom() {
       if (capacity > residents) {
           residents++
           println("You got a room!")
       } else {
           println("Sorry, at capacity and no rooms left.")
       }
   }

   }

/**
* A square cabin dwelling.
*
*  @param residents Current number of residents
*  @param length Length
*/
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
   override val buildingMaterial = "Wood"
   override val capacity = 6

   /**
    * Calculates floor area for a square dwelling.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return length * length
   }

}

/**
* Dwelling with a circular floorspace
*
* @param residents Current number of residents
* @param radius Radius
*/
open class RoundHut(
       residents: Int, val radius: Double) : Dwelling(residents) {

   override val buildingMaterial = "Straw"
   override val capacity = 4

   /**
    * Calculates floor area for a round dwelling.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return PI * radius * radius
   }

   /**
    *  Calculates the max length for a square carpet
    *  that fits the circular floor.
    *
    * @return length of square carpet
    */
    fun calculateMaxCarpetLength(): Double {
        return sqrt(2.0) * radius
    }

}

/**
* Round tower with multiple stories.
*
* @param residents Current number of residents
* @param radius Radius
* @param floors Number of stories
*/
class RoundTower(
       residents: Int,
       radius: Double,
       val floors: Int = 2) : RoundHut(residents, radius) {

   override val buildingMaterial = "Stone"

   // Capacity depends on the number of floors.
   override val capacity = floors * 4

   /**
    * Calculates the total floor area for a tower dwelling
    * with multiple stories.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return super.floorArea() * floors
   }
}

7. 总结

在此 Codelab 中,您学习了如何执行以下操作:

  • 创建一个类层次结构,这是一种包含类的树形结构,其中子级会继承父类的函数。子类继承的有属性和函数。
  • 创建一个 abstract 类,在这种类中,部分函数会留给其子类来实现。因此,abstract 类无法被实例化。
  • 创建 abstract 类的子类。
  • 使用 override 关键字,在子类中替换属性和函数。
  • 使用 super 关键字,引用父类中的函数和属性。
  • 将一个类标记为 open,使其能够被子类化。
  • 将一个属性标记为 private,使其只能在相应类中使用。
  • 使用 with 构造函数,在同一对象实例上进行多次调用。
  • kotlin.math 库导入函数

8. 了解更多内容