Skip to content

Latest commit

 

History

History
70 lines (45 loc) · 7.69 KB

kotlin之继承.md

File metadata and controls

70 lines (45 loc) · 7.69 KB

kotlin之继承

现在我们 要定义一个Student类,每个学生都有自己的学号和年级,因此我们可以 在Student类中加入sno和grade字段。但同时学生也是人呀,学生也会 有姓名和年龄,也需要吃饭,如果我们在Student类中重复定义name、 age字段和eat()函数的话就显得太过冗余了。这个时候就可以让 Student类去继承Person类,这样Student就自动拥有了Person中的字 段和函数,另外还可以定义自己独有的字段和函数。 这就是面向对象编程中继承的思想。

接下来我们尝试用Kotlin 语言实现上述功能。右击com.example.helloworld包→New→Kotlin File/Class,在弹出的对话框中输入“Student”,并选择创建一个Class, 你可以通过上下按键快速切换创建类型。 点击“OK”完成创建,并在Student类中加入学号和年级这两个字段,代码 如下所示:

class Student { var sno = "" var grade = 0 } 

现在Student和Person这两个类之间是没有任何继承关系的,想要让 Student类继承Person类,我们得做两件事才行。

第一件事,使Person类可以被继承。可能很多人会觉得奇怪,尤其是有 Java编程经验的人。一个类本身不就是可以被继承的吗?为什么还要使 Person类可以被继承呢?**这就是Kotlin不同的地方,在Kotlin中任何一个 非抽象类默认都是不可以被继承的,相当于Java中给类声明了final关键 字。**之所以这么设计,其实和val关键字的原因是差不多的,因为类和变量 一样,最好都是不可变的,而一个类允许被继承的话,它无法预知子类会 如何实现,因此可能就会存在一些未知的风险。Effective Java这本书中明 确提到,如果一个类不是专门为继承而设计的,那么就应该主动将它加上 final声明,禁止它可以被继承。 很明显,Kotlin在设计的时候遵循了这条编程规范,默认所有非抽象类都是 不可以被继承的。之所以这里一直在说非抽象类,是因为抽象类本身是无 法创建实例的,一定要由子类去继承它才能创建实例,因此抽象类必须可 以被继承才行,要不然就没有意义了。由于Kotlin中的抽象类和Java中并 无区别,这里我就不再多讲了。 **既然现在Person类是无法被继承的,我们得让它可以被继承才行,方法也 很简单,在Person类的前面加上open关键字就可以了,**如下所示:

open class Person { ... } 

加上open关键字之后,我们就是在主动告诉Kotlin编译器,Person这个类 是专门为继承而设计的,这样Person类就允许被继承了。

第二件事,要让Student类继承Person类。在Java中继承的关键字是 extends,而在Kotlin中变成了一个冒号,写法如下:

class Student : Person() { var sno = "" var grade = 0 } 

继承的写法如果只是替换一下关键字倒也挺简单的,但是为什么Person类 的后面要加上一对括号呢?Java中继承的时候好像并不需要括号。对于初 学Kotlin的人来讲,这对括号确实挺难理解的,也可能是Kotlin在这方面设 计得太复杂了,因为它还涉及主构造函数、次构造函数等方面的知识,这 里我尽量尝试用最简单易懂的讲述来让你理解这对括号的意义和作用,同 时顺便学习一下Kotlin中的主构造函数和次构造函数。 任何一个面向对象的编程语言都会有构造函数的概念,Kotlin中也有,但是 Kotlin将构造函数分成了两种:主构造函数和次构造函数。 主构造函数将会是你最常用的构造函数,每个类默认都会有一个不带参数 的主构造函数,当然你也可以显式地给它指明参数。主构造函数的特点是 没有函数体,直接定义在类名的后面即可。比如下面这种写法:

class Student(val sno: String, val grade: Int) : Person() { }

这里我们将学号和年级这两个字段都放到了主构造函数当中,这就表明在 对Student类进行实例化的时候,必须传入构造函数中要求的所有参数。 比如:

val student = Student("a123", 5)

这样我们就创建了一个Student的对象,同时指定该学生的学号是a123, 年级是5。另外,由于构造函数中的参数是在创建实例的时候传入的,不像 之前的写法那样还得重新赋值,因此我们可以将参数全部声明成val。 你可能会问,主构造函数没有函数体,如果我想在主构造函数中编写一些 逻辑,该怎么办呢?Kotlin给我们提供了一个init结构体,所有主构造函 数中的逻辑都可以写在里面

 class Student(val sno: String, val grade: Int) : Person() { init { println("sno is " + sno) println("grade is " + grade) } } 

这里我只是简单打印了一下学号和年级的值,现在如果你再去创建一个 Student类的实例,一定会将构造函数中传入的值打印出来。 到这里为止都还挺好理解的吧?但是这和那对括号又有什么关系呢?这就 涉及了Java继承特性中的一个规定,子类中的构造函数必须调用父类中的 构造函数,这个规定在Kotlin中也要遵守。 那么回头看一下Student类,现在我们声明了一个主构造函数,根据继承 特性的规定,子类的构造函数必须调用父类的构造函数,可是主构造函数 并没有函数体,我们怎样去调用父类的构造函数呢?你可能会说,在init 结构体中去调用不就好了。这或许是一种办法,但绝对不是一种好办法, 因为在绝大多数的场景下,我们是不需要编写init结构体的。 Kotlin当然没有采用这种设计,而是用了另外一种简单但是可能不太好理解 的设计方式:括号。子类的主构造函数调用父类中的哪个构造函数,在继 承的时候通过括号来指定。因此再来看一遍这段代码,你应该就能理解了 吧。

class Student(val sno: String, val grade: Int) : Person() { }

在这里,Person类后面的一对空括号表示Student类的主构造函数在初始 化的时候会调用Person类的无参数构造函数,即使在无参数的情况下,这 对括号也不能省略。 而如果我们将Person改造一下,将姓名和年龄都放到主构造函数当中,如 下所示:

open class Person(val name: String, val age: Int) { ... }

此时你的Student类一定会报错,这里出现错误的原因也很明显,Person类后面的空括号表示要去调用 Person类中无参的构造函数,但是Person类现在已经没有无参的构造函数了,所以就提示了上述错误。

如果我们想解决这个错误的话,就必须给Person类的构造函数传入name 和age字段,可是Student类中也没有这两个字段呀。很简单,没有就加呗。我们可以在Student类的主构造函数中加上name和age这两个参数, 再将这两个参数传给Person类的构造函数,代码如下所示:

 class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) { ... } 

注意,我们在Student类的主构造函数中增加name和age这两个字段时, 不能再将它们声明成val,因为在主构造函数中声明成val或者var的参数 将自动成为该类的字段,这就会导致和父类中同名的name和age字段造成 冲突。因此,这里的name和age参数前面我们不用加任何关键字,让它的 作用域仅限定在主构造函数当中即可。 现在就可以通过如下代码来创建一个Student类的实例:

 val student = Student("a123", 5, "Jack", 19)