1. 准备工作
前提条件
- 熟悉如何使用 Kotlin Playground 修改 Kotlin 程序。
- 了解本课程第 1 单元中介绍的 Kotlin 编程基本概念,尤其是
main()
程序、能够返回值的带参数的函数、变量、数据类型和操作,以及if/else
语句。 - 能够在 Kotlin 中定义类、通过该类创建对象实例以及访问其属性和方法。
学习内容
- 创建一个利用继承来实现类层次结构的 Kotlin 程序。
- 扩展类、替换其现有函数并添加新函数。
- 为变量选择正确的可见性修饰符。
构建内容
- 一个 Kotlin 程序,包含以类层次结构形式实现的不同类型的住房。
所需条件
- 一台能够连接互联网以访问 Kotlin 园地的计算机
2. 什么是类层次结构?
人们很自然地会将具有相似属性和行为的事物划分为组,甚至会在这些组之间建立某种类型的层次结构。例如,您可以有一个比较宽泛的类别(例如蔬菜),在该类别内,您可以设置更具体的类型(例如豆类)。在豆类中,您可以划分更加具体的类型,例如豌豆、黄豆、扁豆、鹰嘴豆和大豆。
这可以表示为层次结构,因为豆类包含或继承了蔬菜的所有属性(例如,它们属于植物,且可以食用)。同样地,豌豆、黄豆和扁豆均具有豆类的属性,同时也具有自己独特的属性。
我们来看看,如何用编程术语表示这种关系。如果您将 Vegetable
作为 Kotlin 中的一个类,您可以创建 Legume
作为 Vegetable
类的子级或子类。这意味着,Vegetable
类的所有属性和方法均会被 Legume
类继承(即,这些属性和方法同样可用于后者)。
您可以在类层次结构示意图中表示这种关系,如下所示。您可将 Vegetable
称为 Legume
类的父级或父类。
您可以通过创建 Legume
的子类(例如 Lentil
和 Chickpea
)继续扩展类层次结构。这会使 Legume
既是 Vegetable
的子级或子类,同时也是 Lentil
和 Chickpea
的父级或父类。Vegetable
是此层次结构的根类或顶级类(也称为基类)。
Android 类中的继承
正如您在之前的 Codelab 中所做的那样,虽然您可以在不使用类的情况下编写 Kotlin 代码,但 Android 的很多部分都是以类的形式向您提供的,包括 activity、视图和视图组。因此,理解类层次结构是 Android 应用开发的基础,并且有助于您利用 Android 框架提供的诸多功能。
例如,Android 中有一个 View
类,它表示屏幕上的一个矩形区域,并且负责绘制和事件处理。TextView
类是 View
类的子类,这意味着 TextView
会继承 View
类的所有属性和函数,并会添加特定的逻辑,以向用户显示文本。
再往下一级,EditText
和 Button
类是 TextView
类的子级。它们会继承 TextView
和 View
类的所有属性和方法,并会添加自己的特定逻辑。例如,EditText
添加了自己的功能,能够修改屏幕上的文本。
您不必从 View
和 TextView
类中复制所有逻辑,并将其粘贴到 EditText
类中,EditText
可以直接子类化 TextView
类,后者再子类化 View
类。然后,EditText
类中的代码便可专注于使此界面组件不同于其他视图的其他方面。
在 developer.android.com 网站上有关 Android 类的文档页面顶部,您可以看到类层次结构示意图。如果您在层次结构顶部看到 kotlin.Any
,这是因为在 Kotlin 中,所有类都具有一个通用父类 Any。请点击此处了解详情。
如您所见,学习利用类之间的继承关系,可使您的代码更易于编写、复用、阅读和测试。
3. 创建基类
住房的类层次结构
在此 Codelab 中,您将构建一个 Kotlin 程序,该程序会以具有建筑空间、楼层和住客的住房(人们的住处)为例,演示类层次结构的工作方式。
下面是您要构建的类层次结构的示意图。在根部,有一个 Dwelling
,它指定了适用于所有住房的属性和函数,类似于蓝图。然后还有多个类,分别对应方形小木屋 (SquareCabin
)、圆形小屋 (RoundHut
) 和圆形塔楼 (RoundTower
)(即多楼层的 RoundHut
)。
您将实现的类:
Dwelling
:一个表示非特定住房的基类,可存储所有住房通用的信息。SquareCabin
:用木材建成的方形小木屋,建筑平面为方形。RoundHut
:用稻草搭成的圆形小屋,建筑平面为圆形,它是RoundTower
的父级。RoundTower
:用石头建成的多楼层圆形塔楼,建筑平面为圆形。
创建 Dwelling 抽象类
任何类都可以是类层次结构的基类或其他类的父类。
“抽象”类是一种无法实例化的类,因为它没有完全实现。您可以将其视为一个草图。草图包含关于某些事的想法和计划,但其中的信息通常并不足以完成构建。您可以使用草图(抽象类)来创建一个蓝图(类),然后基于后者构建实际的对象实例。
创建父类的一个常见好处是,能够包含其所有子类通用的属性和函数。如果属性的值及函数的实现情况未知,则可以将其作为抽象类。例如,Vegetables
中有许多所有蔬菜通用的属性,但您无法为某种非特定的蔬菜创建实例,因为您不知道关于它的某些信息,例如其形状或颜色。因此,Vegetable
是一个抽象类,每种蔬菜的具体细节交由子类来确定。
抽象类的声明以 abstract
关键字开头。
Dwelling
像 Vegetable
一样,也会是一个抽象类。它将包含很多类住房通用的属性和函数,但属性的确切值和函数的实现细节未知。
- 访问 Kotlin Playground,网址为:https://developer.android.com/training/kotlinplayground。
- 在编辑器中,删除
main()
函数中的println("Hello, world!")
。 - 然后,在
main()
函数下方添加以下代码,以创建一个名为Dwelling
的abstract
类。
abstract class Dwelling(){
}
添加有关建筑材料的属性
在此 Dwelling
类中,您可以定义适用于所有住房的属性,即使对于不同住房来说,这些属性的值可能会有所不同。所有住房都是由某种建筑材料建造而成的。
- 在
Dwelling
中,创建一个类型为String
的buildingMaterial
变量来表示建筑材料。由于建筑材料不会变化,可以使用val
使其成为不可变变量。
val buildingMaterial: String
- 运行程序,系统会显示以下错误消息。
Property must be initialized or be abstract
buildingMaterial
属性没有值。实际上,您不能为其提供值,因为非特定建筑并非由任何特定建筑材料建造而成。因此,如错误消息所示,您可以添加 abstract
关键字作为 buildingMaterial
声明的前缀,以表明该属性不会在此处进行定义。
- 将
abstract
关键字添加到变量定义中。
abstract val buildingMaterial: String
- 运行您的代码,虽然它不会执行任何操作,但现在能顺利编译,不会出现任何错误。
- 在
main()
函数中构建一个Dwelling
实例,然后运行代码。
val dwelling = Dwelling()
- 您将收到一条错误消息,因为您无法创建
Dwelling
抽象类的实例。
Cannot create an instance of an abstract class
- 删除此错误代码。
到目前为止,您已完成的代码如下所示:
abstract class Dwelling(){
abstract val buildingMaterial: String
}
添加有关可容纳人数的属性
住房的另一个属性是可容纳人数,即屋内可以住多少人。
所有住房的可容纳人数都不会变。但是,不能在 Dwelling
父类中设置可容纳人数,而应在特定类型住房对应的子类中进行定义。
- 在
Dwelling
中,添加一个名为capacity
的abstract
整数val
。
abstract val capacity: Int
添加有关住客人数的私有属性
所有住房都有一定数量的 residents
(即住在该住房里的人数,该值可能小于或等于 capacity
),因此应在 Dwelling
父类中定义 residents
属性,以便所有子类继承和使用。
- 您可以将
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
能容纳其他住客,函数应按这种比较方法返回 true
或 false
。
- 将
hasRoom()
函数添加到Dwelling
类。
fun hasRoom(): Boolean {
return residents < capacity
}
- 您可以运行此代码,系统应该不会显示错误。该代码尚未执行任何可见操作。
完成后的代码应如下所示:
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
4. 创建子类
创建 SquareCabin 子类
- 在
Dwelling
类下,创建一个名为SquareCabin
的类。
class SquareCabin
- 接下来,您需要指明
SquareCabin
与Dwelling
相关。在您的代码中,您需要表明SquareCabin
扩展自Dwelling
(或者是Dwelling)
的子类),因为SquareCabin
将会提供Dwelling
抽象部分的实现。
通过在 SquareCabin
类名称后添加一个冒号 (:
),后跟用于初始化父级 Dwelling
类的调用,表明此继承关系。不要忘记在 Dwelling
类名称后添加圆括号。
class SquareCabin : Dwelling()
- 如果是从父类扩展,您必须传入父类所需的参数。
Dwelling
需要输入residents
人数。您可以传入固定的住客人数,例如3
。
class SquareCabin : Dwelling(3)
不过,您希望您的程序能够更加灵活,并且允许 SquareCabins
拥有不同的住客人数。因此,在 SquareCabin
类定义中将 residents
作为一个参数。不要将 residents
声明为 val,
,因为您是在重复使用已在父类 Dwelling
中声明的属性。
class SquareCabin(residents: Int) : Dwelling(residents)
- 运行您的代码。
- 这会导致出现错误。我们来看一下:
Class 'SquareCabin' is not abstract and does not implement abstract base class member public abstract val buildingMaterial: String defined in Dwelling
如果您声明抽象函数和变量,就表示您承诺稍后将为它们提供值和实现。对于变量,这意味着该抽象类的任何子类都需要为其提供值。对于函数,这意味着任何子类都需要实现函数主体。
在 Dwelling
类中,您定义了一个 abstract
变量 buildingMaterial
。SquareCabin
是 Dwelling
的子类,因此它必须为 buildingMaterial
提供值。使用 override
关键字来表明此属性是在父类中定义的,并且在此类中将被替换掉。
- 在
SquareCabin
类中,override
buildingMaterial
属性并为其分配值"Wood"
。 - 针对
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
- 在
Dwelling
和SquareCabin
类定义前插入一个空main()
函数。
fun main() {
}
abstract class Dwelling(private var residents: Int) {
...
}
class SquareCabin(residents: Int) : Dwelling(residents) {
...
}
- 在
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
类中定义的。由于 SquareCabin
是 Dwelling
类的子类,hasRoom()
函数是未经任务操作直接继承而来的。现在,可以在 SquareCabin
的所有实例上调用 hasRoom()
函数,如代码段 squareCabin.hasRoom()
所示。
- 运行代码,输出内容应如下所示:
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
}
- 在
main()
函数中,将您的输出语句改为使用with
。 - 删除输出语句中的“
squareCabin.
”。
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
}
- 再次运行代码,以确保它能够正常运行而不会出现任何错误,并输出相同的结果。
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 子类
- 按照与添加
SquareCabin
相同的方式,为Dwelling
添加另一个子类RoundHut
。 - 替换
buildingMaterial
并为其提供值"Straw"
。 - 替换
capacity
并将其设置为 4。
class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
- 在
main()
中,创建一个住客人数为 3 的RoundHut
实例。
val roundHut = RoundHut(3)
- 添加以下代码以输出
roundHut
的相关信息。
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
- 运行代码,整个程序的输出应如下所示:
Square Cabin ============ Capacity: 6 Material: Wood Has room? false Round Hut ========= Material: Straw Capacity: 4 Has room? true
现在,您有了一个如下所示的类层次结构,其中 Dwelling
为根类,SquareCabin
和 RoundHut
为 Dwelling
的子类。
创建 RoundTower 子类
这个类层次结构中处于最终层级的类是圆形塔楼。您可以将圆形塔楼视为一个由石头建造而成的多楼层圆形小屋。因此,您可以将 RoundTower
作为 RoundHut
的子类。
- 创建一个
RoundTower
类,它是RoundHut
的子类。将residents
参数添加到RoundTower
的构造函数中,然后将该参数传递给RoundHut
父类的构造函数。 - 将
buildingMaterial
替换为"Stone"
。 - 将
capacity
设置为4
。
class RoundTower(residents: Int) : RoundHut(residents) {
override val buildingMaterial = "Stone"
override val capacity = 4
}
- 运行此代码,您会收到一条错误消息。
This type is final, so it cannot be inherited from
此错误意味着,RoundHut
类不能被子类化(或被继承)。默认情况下,在 Kotlin 中,类是最终层级,无法被子类化。您只能从 abstract
类或标记有 open
关键字的类继承。因此,您需要使用 open
关键字标记 RoundHut
类,使其能够被继承。
- 在
RoundHut
声明的开头添加open
关键字。
open class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
- 在
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
}
- 运行您的代码。它现在应该能够正常运行而不会出现任何错误,并且会生成以下输出。
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
修改为多楼层,并根据楼层数量调整可容纳人数。
- 更新
RoundTower
构造函数,以接受另一个整数参数val floors
作为楼层数量。将其置于residents
之后。请注意,您无需将此参数传递给父级RoundHut
构造函数,因为floors
是在RoundTower
中定义的,而RoundHut
没有floors
。
class RoundTower(
residents: Int,
val floors: Int) : RoundHut(residents) {
...
}
- 运行您的代码。在
main()
方法中创建roundTower
时出现错误,因为您没有为floors
参数提供数字。您可以添加缺少的参数。
或者,您也可以在 RoundTower
的类定义中为 floors
添加默认值,如下所示。然后,在没有 floors
值传递到构造函数的情况下,系统就可以使用默认值创建对象实例。
- 在您的代码中,在
floors
声明后添加= 2
,以为其分配默认值 2。
class RoundTower(
residents: Int,
val floors: Int = 2) : RoundHut(residents) {
...
}
- 运行您的代码。它应该能顺利编译,因为
RoundTower(4)
现在会使用 2 层楼的默认值创建RoundTower
对象实例。 - 在
RoundTower
类中,更新capacity
,用之前的值乘以楼层数。
override val capacity = 4 * floors
- 运行代码,请注意,
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()
- 首先,向
Dwelling
类中添加一个abstract
floorArea()
函数。返回Double
。Double 与String
和Int
一样,是一种数据类型;它可用于浮点数(即带有小数点且后跟小数部分的数字,例如 5.8793)。
abstract fun floorArea(): Double
抽象类中定义的所有抽象方法都必须在其任意子类中实现。您需要先在子类中实现 floorArea()
,然后才能运行代码。
针对 SquareCabin 实现 floorArea()
与实现 buildingMaterial
和 capacity
一样,由于您要实现的 abstract
函数是在父类中定义的,您需要使用 override
关键字。
- 在
SquareCabin
类中,首先输入关键字override
,后跟floorArea()
函数的实际实现,如下所示。
override fun floorArea(): Double {
}
- 返回计算出的建筑面积。矩形或方形的面积计算方法为,一边的长度乘以另一边的长度。函数主体为
return length * length
。
override fun floorArea(): Double {
return length * length
}
在此类中,长度是一个变量,每个实例的长度各不相同,因此您可以将其添加为 SquareCabin
类的构造函数参数。
- 更改
SquareCabin
的类定义,以添加Double
类型的length
参数。将该属性声明为val
,因为建筑的长度不会变化。
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
Dwelling
具有构造函数参数 residents
,因此其所有子类都具有该参数。由于这是 Dwelling
构造函数中的第一个实参,所以最好也将其作为所有子类构造函数中的第一个实参,并在所有类定义中以相同的顺序排列实参。因此,在 residents
参数后插入新的 length
参数。
- 在
main()
中,更新创建squareCabin
实例的代码。将50.0
作为length
传入SquareCabin
构造函数。
val squareCabin = SquareCabin(6, 50.0)
- 在
squareCabin
的with
语句中,添加针对建筑面积的输出语句。
println("Floor area: ${floorArea()}")
您的代码将不会运行,因为您还需要在 RoundHut
中实现 floorArea()
。
针对 RoundHut 实现 floorArea()
以同样的方式针对 RoundHut
实现建筑面积。RoundHut
也是 Dwelling
的直接子类,因此您需要使用 override
关键字。
圆形住房建筑面积的计算方法为,PI * 半径 * 半径。
PI
是一个数学值。它会在数学库中进行定义。库是一个预定义的函数和值集合,在程序外部定义,但可供程序使用。为了使用某个库函数或值,您需要告知编译器您将使用该函数或值。方法是将该函数或值导入到程序中。如需在程序中使用 PI
,您需要导入 kotlin.math.PI
。
- 从 Kotlin 数学库导入
PI
。将此导入语句放在文件的顶部,main()
之前。
import kotlin.math.PI
- 针对
RoundHut
实现floorArea()
函数。
override fun floorArea(): Double {
return PI * radius * radius
}
警告:如果您不导入 kotlin.math.PI,将会遇到错误,因此请在使用之前导入此库。或者,您可以写出 PI 的完全限定版本,如“kotlin.math.PI * 半径 * 半径”中所示,然后便无需添加导入语句。
- 更新
RoundHut
构造函数以传入radius
。
open class RoundHut(
residents: Int,
val radius: Double) : Dwelling(residents) {
- 在
main()
中,通过向RoundHut
构造函数传入radius
10.0
,更新roundHut
的初始化。
val roundHut = RoundHut(3, 10.0)
- 在
roundHut
的with
语句中添加输出语句。
println("Floor area: ${floorArea()}")
针对 RoundTower 实现 floorArea()
您的代码尚无法正常运行,它会失败并显示以下错误消息:
Error: No value passed for parameter 'radius'
在 RoundTower
中,为了使程序能够顺利编译,您不需要实现 floorArea()
,因为它能够从 RoundHut
继承,但您需要更新 RoundTower
类定义,使其具有与其父级 RoundHut
相同的 radius
参数。
- 更改 RoundTower 构造函数,使其同样接受
radius
。将radius
放在residents
之后、floors
之前。建议将具有默认值的变量列在末尾。请记得将radius
传递给父类构造函数。
class RoundTower(
residents: Int,
radius: Double,
val floors: Int = 2) : RoundHut(residents, radius) {
- 在
main()
中更新初始化roundTower
的代码。
val roundTower = RoundTower(4, 15.5)
- 添加一个调用
floorArea()
的输出语句。
println("Floor area: ${floorArea()}")
- 现在,您可以运行代码了!
- 请注意,
RoundTower
的计算方法不正确,因为它继承自RoundHut
,并未考虑floors
数量。 - 在
RoundTower
中,override floorArea()
以便为其提供不同的实现,将面积乘以楼层数量。注意,您可以在抽象类 (Dwelling
) 中定义一个函数,在子类 (RoundHut
) 中实现它,然后在子类的子类 (RoundTower
) 中再次替换它。这是一种两全其美的做法,您既可以继承自己需要的函数,又能替换掉自己不需要的函数。
override fun floorArea(): Double {
return PI * radius * radius * floors
}
此代码能够正常运行,不过有一种方法可以避免反复使用父类 RoundHut
中已有的代码。您可以从父类 RoundHut
中调用 floorArea()
函数,后者会返回 PI * radius * radius
。然后将该结果乘以 floors
数。
- 在
RoundTower
中,将floorArea()
更新为使用其父类中的floorArea()
实现。使用super
关键字,调用父级中定义的函数。
override fun floorArea(): Double {
return super.floorArea() * floors
}
- 再次运行您的代码,
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。
- 在
Dwelling
类中实现getRoom()
函数。
fun getRoom() {
if (capacity > residents) {
residents++
println("You got a room!")
} else {
println("Sorry, at capacity and no rooms left.")
}
}
- 向
roundHut
的with
语句块添加一些输出语句,以同时使用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.
如需了解详情,请参阅解决方案代码。
为圆形住房准备合适的地毯
假设您需要知道要为自己的 RoundHut
或 RoundTower
准备多大边长的地毯。将相应函数放到 RoundHut
中,使其可用于所有圆形住房。
- 首先,从
kotlin.math
库导入sqrt()
函数。
import kotlin.math.sqrt
- 在
RoundHut
类中实现calculateMaxCarpetLength()
函数。用于计算适合放入圆形中的方形地毯长度的公式为sqrt(2) * radius
。上图对此进行了说明。
fun calculateMaxCarpetLength(): Double {
return sqrt(2.0) * radius
}
将 Double
值 2.0
传入数学函数 sqrt(2.0)
,因为该函数的返回值类型是 Double
,而不是 Integer
。
- 现在,
calculateMaxCarpetLength()
方法可以在RoundHut
和RoundTower
实例上调用了。在main()
函数中,为roundHut
和roundTower
添加输出语句。
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. 了解更多内容
- 继承
- 类和继承
- 构造函数
- 继承(抽象、公开、替换、私有)
with
(正式定义)