中级教程: 开放类与特殊类
在这一章中, 你将学习开放类, 它们如何与接口一起工作, 以及 Kotlin 中其他特殊类型的类.
开放类
如果你不能使用接口或抽象类, 你可以将一个类声明为 open, 明确的让它能够被继承. 方法是, 在你的类声明之前使用 open
关键字:
要创建一个从另一个类继承的类, 请在你的类头部之后添加一个冒号, 然后调用你想要继承的父类的构造器:
这个示例中, Car
类继承 Vehicle
类:
和创建普通类的实例一样, 如果你的类继承一个父类, 那么它必须初始化父类头部中定义的所有参数. 因此在这个示例中, Car
类的 car
实例初始化父类参数: make
和 model
.
覆盖继承的行为
如果你想要从一个类继承, 但改变某些行为, 你可以覆盖继承的行为.
默认情况下, 不能覆盖父类的成员函数或属性. 与抽象类一样, 你需要添加特殊的关键字.
成员函数
要让父类中的函数能够被覆盖, 请在父类中它的声明之前使用 open
关键字:
要覆盖一个继承的成员函数, 请在子类中函数声明之前使用 override
关键字:
例如:
在这个示例中:
Car
类继承自Vehicle
类, 创建Car
类的 2 个实例:car1
和car2
.在
Car
类中, 覆盖displayInfo()
函数, 也打印输出车门数量.对
car1
和car2
实例调用覆盖的displayInfo()
函数.
属性
在 Kotlin 中, 使用 open
关键字让一个属性能够继承, 并在之后覆盖它, 这样的方法不是常见的做法. 大多数情况下, 使用抽象类或接口, 其中的属性默认能够继承.
开放类中的属性能够被子类访问. 一般来说, 最好直接访问属性, 而不要用新的属性覆盖它们.
例如, 假设你有一个属性 transmissionType
, 你想要之后覆盖它. 覆盖属性的语法与覆盖成员函数完全一样. 你可以这样做:
但是, 这不是好的做法. 相反, 你可以将属性添加到可继承的类的构造中, 并在创建 Car
子类时声明它的值:
直接访问属性, 而不是覆盖, 可以让代码更加简单, 更加易读. 只在父类中声明属性一次, 然后通过构造器传递属性值, 就不再需要在子类中进行覆盖.
关于类的继承, 以及覆盖类的行为, 详情请参见 继承.
开放类与接口
你可以创建一个类, 它继承一个类 并且 实现多个接口. 这种情况下, 你必须在冒号之后先声明父类, 然后列出接口:
特殊类
除抽象类, 开放类, 数据类之外, Kotlin 还有一些特殊类型的类, 是为各种目的设计的, 例如限制特定的行为, 或减少创建小对象时的性能损失.
封闭类(Sealed Class)
有些时候你可能会想要限制继承. 你可以使用封闭类(Sealed Class)来实现. 封闭类是一个特殊类型的 抽象类. 一旦将一个类声明为封闭, 就只能在同一个包之内创建它的子类. 在这个范围之外, 不能继承封闭类.
要创建一个封闭类, 请使用 sealed
关键字:
封闭类在与 when
表达式一起使用时特别有用. 使用 when
表达式, 你可以对所有可能的子类定义行为. 例如:
在这个示例中:
有一个封闭类
Mammal
, 构造器参数为name
.Cat
类继承Mammal
封闭类, 并使用来自Mammal
类的name
参数, 作为它自己的构造器中的catName
参数.Human
类继承Mammal
封闭类, 并使用来自Mammal
类的name
参数, 作为它自己的构造器中的humanName
参数. 它的构造器中还有job
参数.greetMammal()
函数接受Mammal
类型的参数, 并返回一个字符串.在
greetMammal()
的函数 body 部, 有一个when
表达式, 使用is
操作符 检查mammal
的类型, 决定执行哪个动作.main()
函数调用greetMammal()
函数, 使用Cat
类的一个实例,name
参数为Snowy
.
关于封闭类, 以及推荐的使用场景, 详情请参见 封闭类与封闭接口.
枚举类(Enum Class)
当你想用一个类表达一组有限的, 不同的值, 适合使用枚举类(Enum Class). 一个枚举类包含一些枚举常数, 枚举常数自身又是枚举类的实例.
要创建枚举类, 请使用 enum
关键字:
假设你想要创建一个枚举类, 包含一个进程的不同状态. 各个枚举常数必须使用逗号 ,
分隔:
State
枚举类包含枚举常数: IDLE
, RUNNING
, 和 FINISHED
. 要访问一个枚举常数, 请使用类名称, 加上 .
, 再加上枚举常数的名称:
你可以在 when
表达式中使用这个枚举类, 根据枚举常数的值定义要执行的动作:
和通常的类一样, 枚举类可以拥有属性和成员函数.
例如, 假设你在使用 HTML, 想要创建一个枚举类, 包含一些颜色. 你想要每个颜色拥有一个属性, 假设叫做 rgb
, 其中包含它们的 16 进制 RGB 值. 在创建枚举常数时, 你必须使用这个属性来初始化它:
要添加对这个类一个成员函数, 将函数与枚举常数用分号 ;
分隔:
在这个示例中, containsRed()
成员函数使用 this
关键字访问枚举常数的 rgb
属性的值, 并检查 16 进制值的最先头的位是否包含 FF
, 并返回一个 boolean 值.
详情请参见 枚举类.
内联的值类(Inline Value Class)
有时候在你的代码中, 你可能想要创建小的类对象, 而且只是短暂的使用它们. 这种方法可能造成性能损失. 内联的值类(Inline Value Class) 是一种特殊类型的类, 可以避免这样的性能损失. 但是, 它们只能包含值.
要创建一个内联的值类, 请使用 value
关键字, 以及 @JvmInline
注解:
内联的值类 必须 拥有单个属性, 在类的 header 部初始化.
假设你想要创建一个类, 收集 EMail 地址:
在这个示例中:
Email
是一个内联的值类, 在类的 header 部有一个属性:address
.sendEmail()
函数接受Email
类型的对象作为参数, 并向标准输出打印一个字符串.main()
函数:创建
Email
类的一个实例email
.对
email
对象调用sendEmail()
函数.
通过使用内联的值类, 你让你的类成为内联的, 可以在代码中直接使用它, 而不必创建对象. 这样可以显著的减少内存使用量, 并改善你的代码的运行时性能.
关于内联的值类, 详情请参见 内联的值类.
实际练习
习题 1
你管理着一家快递公司, 需要一种方法来追踪包裹的状态. 请创建一个封闭类 DeliveryStatus
, 包含数据类, 表示以下状态: Pending
, InTransit
, Delivered
, Canceled
. 请完成 DeliveryStatus
类的声明, 让 main()
函数中的代码运行成功:
习题 2
在你的程序中, 需要处理不同状态和类型的错误. 你有一个封闭类, 捕捉数据类或对象中声明的各种状态. 请完成下面的代码, 创建枚举类 Problem
, 表示不同的问题类型: NETWORK
, TIMEOUT
, and UNKNOWN
.