Kotlin开发笔记:类层次结构和继承
简介
面向对象编程的语言中,关于对象层次的抽象是很重要的,这会涉及到类层次的结构,接口和继承等内容,本篇文章将会介绍与之相关的内容,包括:
-
- 接口的相关内容
-
- 抽象类的相关内容
-
- 嵌套类和内部类
-
- 继承
-
- Sealed类
-
- 枚举类型的相关内容
接口
Kotlin中的接口特性
Kotlin中的接口和Java中的接口在语义上很类似,都是用来实现契约式设计的工具。接口作为规范,类作为这些契约式的实现者。但是在语法上又和Java有很大不同,具体来说,Kotlin中的接口更接近于类,限制更少。比如说,我们可以在Kotlin中的接口中实现方法而不需要使用default关键字,而且我们可以在接口中创建伴生对象,所以我们可以在接口中实现静态方法或者创建静态变量。
创建接口
Kotlin中创建接口和Java也非常类似,都是使用interface关键字,比如我们可以创建一个名为Remote的接口:
interface Remote{
fun up()
fun down()
fun doubleup(){
up()
up()
}
}
该接口中包含两个抽象方法和一个实现了的方法,这个实现了的方法又依赖于抽象方法。但是仔细一想,我们实现了抽象方法,这个实现了的方法也自然实现了。
接下来,我们将伴生对象运用到接口中:
interface Remote{
fun up()
fun down()
fun doubleup(){
up()
up()
}
companion object{
val id:Int = 0
fun combine(first:Remote,second:Remote):Remote = object : Remote {
override fun up() {
first.up()
second.up()
}
override fun down() {
first.down()
second.down()
}
}
}
}
这个伴生对象中的方法就是将两个实现了Remote接口进行合并,然后返回这个合并了的接口。
实现接口
Kotlin中接口的实现也很简单,就是在类的最后用:跟上要实现的接口
class Tv{
var volume = 0
}
class TvRemote(val tv: Tv):Remote{
override fun up() {
tv.volume++
}
override fun down() {
tv.volume--
}
}
当然我们也可以使用匿名类来实现,我们用两个匿名对象实现然后测试一下上文提到的combine函数:
fun main() {
val remote = TvRemote(Tv())
println("volume is ${remote.tv.volume}")
remote.doubleup()
println("volume is ${remote.tv.volume}")
remote.down()
println("volume is ${remote.tv.volume}")
val re1 = object:Remote{
override fun up() {
println("r1 up")
}
override fun down() {
println("r1 down")
}
}
val re2 = object:Remote{
override fun up() {
println("r2 up")
}
override fun down() {
println("r2 down")
}
}
val re3 = Remote.combine(re1,re2)
re3.up()
re3.down()
}
最后我们输出的结果:
这里就成功将r1和r2进行了合并。
抽象类
创建抽象类
Kotlin中的抽象类和Java中的抽象类也很类似,也是用abstract修饰符进行修饰的,这里给出一个示例:
abstract class Musician(val name:String,val activeFrom:Int){
abstract fun instrumentType():String
}
class Cellist(name:String,activeFrom:Int):Musician(name, activeFrom){
override fun instrumentType(): String = "String"
}
fun main() {
val ma = Cellist("Yo-Yo-Ma",1961)
}
可以看到抽象类和抽象类中的抽象方法都要用abstract修饰符进行修饰,后面要实现这个抽象类的需要在冒号后面跟上抽象类的构造方法。在这个示例中Cellist类的构造方法接收两个参数并将这些参数传递给抽象类的构造方法。而且我们在Cellist中实现了抽象方法,实现方法要用override修饰符。
抽象类和接口
抽象类和接口的主要区别在于:
- 在接口中定义的属性没有幕后字段(即使用val或者var之后自动生成的字段),他们必须依赖抽象方法或者伴生对象来从实现类中得到属性。另外,抽象类中的属性可以使用幕后字段。
- 可以实现多个接口但是最多只能继承一个抽象类。
关于到底是使用接口还是抽象类,书中也给出了建议:接口不能包含字段但是类可以实现多个接口。另外,抽象基类可以有字段,但是一个类最多只能从一个抽象类拓展。所以每个方法都有利有弊。
如果想在多个类之间重用状态,那么抽象类是一个不错的选择。可以在抽象类中实现公共状态,并让实现类重写方法,同时重用抽象类提供的状态。
如果希望多个类遵守一个或者多个契约或规范,但又希望这些类选择自己的实现,那么接口是更好的选择。
但是在Kotlin和现代Java中,接口比抽象类稍微有一些优势。 接口能够进行方法实现,但是没有状态。一个类可以实现多个接口。在可能的情况下,最好使用接口而不是抽象类,这样会提供到更多的灵活性。
嵌套类和内部类
在Java中,如果我们想要在一个类中定义其内部类,只需要在简单的在外部类中再定义一个类即可:
class outer{
class inner{
}
}
这样就在outer类中创建了一个内部类。而如果要创建静态内部类的话只需要在内部类的class关键字前加上static修饰符即可。而在Kotlin中没有明确的静态内部类的概念,取而代之的是嵌套类。原来普通的内部类对应的是Kotlin中的内部类。
嵌套类
我们先来介绍Kotlin中的嵌套类,Kotlin中的嵌套类类似于Java中的静态内部类,嵌套类和外部类的关系较为独立,大部分情况下没有耦合调用的情况。比如说在Java中静态内部类的构造方法调用是不用依赖于外部类的实例的,Kotlin中的嵌套类也是这样。 我们先来给出一个嵌套类的示例:
class inout {
var counter = 0
val mIn = innn()
override fun toString(): String {
return "inout"
}
class stClass{
}
}
在这个示例中外部类为inout,嵌套类为stClass。调用stClass的方法不用依赖于inout类的实例,比如:
fun main() {
val st = inout.stClass()
}
可以看到,调用方式就是Java中调用静态内部类的方法。当然,这种嵌套类和静态内部类也有相同的限制,那就是其无法访问外部类中的参数,因为创建嵌套类的实例时无法保证一定有外部类的实例被创建出来。
内部类
与Java中的内部类对应,Kotlin中的内部类修饰的区别就是在嵌套类前面加上inner修饰符。与嵌套类不同,内部类的方法和成员的调用是依赖于外部类的实例的,也就是说,如果有一个内部类的实例那就必然有一个外部类的实例。除此之外,内部类可以引用外部类的方法和变量等。下面我们来写一个内部类的实例:
class inout {
var counter = 0
val mIn = innn()
override fun toString(): String {
return "inout"
}
inner class innn{
public fun showOuter(){
val st = this@inout.toString()
println(st)
}
}
}
接下来我们调用这个内部类:
fun main() {
inout().innn().showOuter()
}
可以看到在调用内部类的方法时就必须要先创建一个外部类的实例。再仔细观察innn类的showOuter方法,这个方法引用了外部类的toString方法,这是inner修饰符赋予它的权限。此处还要接着介绍一下Kotlin中的this语法。普通的this指代的是当前类的实例,就这个例子来说,单个this指代的就是内部类innn的实例。而this@inout可以翻译为"this of inout",也就是外部类inout的实例。所以这里的this@inout.toString方法最终调用到的就是外部类的toString方法。同理,除了this之外我们还可以使用super关键字,语法方面是和this一致的。单个super指的就是当前类实例的基类,super@ *** 指代的就是 *** 的超类,不过不推荐使用这种语法。
匿名内部类
最后再让我们介绍一下Kotlin中的匿名内部类。其实关于匿名对象我们在之前的内容中已经介绍过了,就是用object关键字来定义。不过匿名内部类和内部类不同,它不需要用inner修饰。这里演示一个实现了Remote接口的匿名内部类:
class Tv{
private var volume = 0
val remote:Remote
get() = object:Remote{
override fun up() {
volume++
}
override fun down() {
volume--
}
override fun toString(): String {
return "Remote: ${this@Tv.toString()}"
}
}
override fun toString(): String {
return "Volume: ${volume}"
}
}
这里在类的内部定义了一个实现了Remote接口的成员变量remote,并且之后设置了它的get方法。get方法的逻辑也很简单,就是每次都新创建一个实现了Remote接口的匿名内部类。这代码实际在每次访问Tv类的remote变量都会创建一个新的匿名内部类,每个创建的匿名内部类的地址也不同。最后匿名内部类也可以使用this和super语法。
继承
关于继承的内容我们在之前的抽象类的内容中有所提及,具体来说就是用冒号代表继承。在Kotlin中的继承是有一层额外的防御机制的,就如同变量要指定val还是var一样,Kotlin中的类也要指定是否可以被继承,一个类在默认情况下是不允许被继承的。
在Kotlin中用open关键字和final关键字来表示类是否可以被继承。如果一个类可以被继承,就必须写上open关键字:
open class Vehicle(val year:Int,open var color:String) {
}
但是如果不想让一个类被继承可以不写(默认情况下就是final修饰的)或者写上final关键字。并且,如果你想让一个类中的某个字段或者方法可以被重写的话就要指定该字段或者方法为open,并且在子类中还要标明重写方法关键字override:
open class Vehicle(val year:Int,open var color:String) {
//允许重写字段--实际上是允许重写访问器和设置器
open val km = 0
final override fun toString(): String {
return "year: $year,Color : $color,Km : $km"
}
fun repaint(newColor:String){
color = newColor
}
}
比如说这个类,我们用open关键字修饰Vehicle类表明这个类允许被继承。用open关键字修饰km字段。由于toString方法是Any类中的open方法,所以我们重写这个方法时需要用override关键字,同时我们又希望这个方法不要再被其子类重写了,所以加上final关键字,这样继承Vehicle类的类将无法重写toString方法。最后的一个repaint方法没有标明是open还是final的,这种情况下默认是用final修饰,所以子类将无法重写repaint方法。
重写字段
在上面的示例中我们提到了重写字段,这是一个比较新颖的概念,什么是重写字段呢?在之前的文章中,我们介绍了Kotlin中的访问字段实际上并不是直接访问该字段,而是用字段的get和set方法实现的,所以这里的重写字段的意思实际上就是重写字段的get和set方法。同时,我们可以在重写在类或者构造函数的参数列表中定义的属性,基类中的val属性可以用派生类中的var或者val来重写,而var属性只能用var属性重写。
接下来我们用一个子类来演示:
open class Car(year: Int,color: String) : Vehicle(year, color){
override var km: Int = 0
set(value) {
if (value < 1){
throw java.lang.RuntimeException("不能为负数")
}
field = value
}
fun drive(distance:Int){
km += distance
}
}
这里我们重写了km字段,将其属性从val改为var,并且重写了其set方法,使其在km值小于1时抛出异常。同时这里也表明了该如何实现继承类,就是在子类后面跟上父类的构造方法,Car构造函数中的参数将传递给Vehicle的构造函数。
Sealed类
Sealed类是Kotlin中用于实现继承的特殊基类。Sealed类是作为一个基层地带来给同文件的其他类实现继承的。具体来讲,Kotlin中的Sealed类对于同一个文件中定义的其他类进行拓展是开放的,但对于其他文件的类来说是封闭的。
sealed class Card(val suit:String)
class ace(suit: String):Card(suit)
class King(suit:String):Card(suit){
override fun toString(): String {
return "King of $suit"
}
}
class Queen(suit:String):Card(suit){
override fun toString(): String {
return "Queen of $suit"
}
}
class Pip(suit:String,val number:Int):Card(suit){
init {
if(number < 2 || number > 10){
throw java.lang.RuntimeException("Error")
}
}
}
上面例子中的Card类就是一个Sealed类,是用于该文件中所有其他类的基类,Sealed类的构造函数没有标记为private但是默认是private。
枚举类
枚举类在Kotlin中十分简单,是用enum关键字修饰的,这里直接给出一个示例来说明,我们在前面的Vehicle的基础上进行修改:
open class Vehicle constructor(val year:Int,open var color:Color) {
//允许重写字段--实际上是允许重写访问器和设置器
open val km = 0
final override fun toString(): String {
return "year: $year,Color : $color,Km : $km"
}
fun repaint(newColor:Color){
color = newColor
}
}
open class Car(year: Int,color: Color) : Vehicle(year, color){
override var km: Int = 0
set(value) {
if (value < 1){
throw java.lang.RuntimeException("不能为负数")
}
field = value
}
fun drive(distance:Int){
km += distance
}
}
enum class Color(val c:Char){RED('r'),YELLOW('y'),BLUE('b'),GREEN('g');
override fun toString(): String {
return when(c){
'r' -> "红色"
'y' -> "黄色"
'b' -> "蓝色"
'g' -> "绿色"
else -> "non"
}
}
}
fun main() {
val car = Car(2023,Color.RED)
car.drive(10)
println(car.toString())
}
这里我们创建了一个颜色枚举类Color并且声明了四种颜色实例,并且重写了其toString方法。这里注意我们要写枚举类的方法的话需要用分号将值列表与方法区分开来。除此之外我们还可以给枚举类来记录属性,只需要先在构造方法中声明幕后字段然后在值列表后面跟上相应的值即可。
最后运行出来的结果: