💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 类与继承 [TOC] ## 类 Kotlin 中使用关键字`class`声明类 ```kotlin class Invoice { /*……*/ } ``` 类声明由**类名**、**类头**(指定其类型参数、主构造函数等)以及由花括号包围的**类体**构成。**类头与类体都是可选**的; 如果一个类没有类体,可以省略花括号。 ```kotlin class Empty ``` ### 构造函数 在 Kotlin 中的一个类可以有一个**主构造函数**以及一个或多个**次构造函数**。 * 主构造函数是类头的一部分:它跟在类名(与可选的类型参数)后。 ```kotlin class Person constructor(firstName: String) { /*……*/ } ``` 如果主构造函数没有任何注解或者可见性修饰符(默认的可见性修饰符时`public`。可以省略不写),可以省略这个 *constructor*关键字。 ```kotlin class Person(firstName: String) { /*……*/ } ``` * 主构造函数不能包含任何的代码。初始化的代码可以放到以 *init*关键字作为前缀的**初始化块(initializer blocks**)中。 在实例初始化期间,初始化块按照它们出现在类体中的顺序执行,与属性初始化器交织在一起: ```kotlin //sampleStart class InitOrderDemo(name: String) { val firstProperty = "First property: $name".also(::println) init { // 主构造函数参数可以在初始化块中使用 println("First initializer block that prints ${name}") } val secondProperty = "Second property: ${name.length}".also(::println) init { println("Second initializer block that prints ${name.length}") } } //sampleEnd fun main() { InitOrderDemo("hello") } ``` >[info]**注意**,主构造的参数可以在初始化块中使用。它们也可以在类体内声明的属性初始化器中使用: ```kotlin class Customer(name: String) { // 主构造函数参数也可以在属性初始化器中使用 val customerKey = name.toUpperCase() } ``` * 事实上,声明属性以及从主构造函数初始化属性(在主构造函数中声明类属性(类成员变量/字段)以及初始化属性),Kotlin 有简洁的语法: ```kotlin class Person(val firstName: String, val lastName: String, var age: Int) { /*……*/ } ``` 与普通属性一样,主构造函数中声明的属性可以是可变的(*var*)或只读的(*val*)。 如果构造函数有注解或可见性修饰符,这个 *constructor*关键字是必需的,并且这些修饰符在它前面: ```kotlin class Customer public @Inject constructor(name: String) { /*……*/ } ``` 更多详情,参见[可见性修饰符](http://www.kotlincn.net/docs/reference/visibility-modifiers.html#%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0) #### 次构造函数 类也可以声明前缀有 *constructor*的**次构造函数**: ```kotlin class Person { var children: MutableList<Person> = mutableListOf<Person>(); constructor(parent: Person) { parent.children.add(this) } } ``` * **同时存在主构造函数和二级构造函数时的情况** 如果类有一个主构造函数,每个次构造函数**需要委托给主构造函数**,可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用 *this*关键字即可: ```kotlin class Person(val name: String) { var children: MutableList<Person> = mutableListOf<Person>(); constructor(name: String, parent: Person) : this(name) { parent.children.add(this) } } ``` 请注意,初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句,因此**所有初始化块中的代码都会在次构造函数体之前执行。即使该类没有主构造函数,这种委托仍会隐式发生,并且仍会执行初始化块**: ```kotlin //sampleStart class Constructors { init { println("Init block") } constructor(i: Int) { println("Constructor") } } //sampleEnd fun main() { Constructors(1) } ``` 运行结果 ``` Init block Constructor ``` * **当类的主构造函数都存在默认值时的情况** 如果一个非抽象类没有声明任何(主或次)构造函数,它会生成一个不带参数的主构造函数,构造函数的可见性是 public。如果你不希望你的类有一个公有构造函数,你需要声明一个带有非默认可见性(比如private)的空的主构造函数: ```kotlin class DontCreateMe private constructor () { /*……*/ } ``` > **注意**:在 JVM 上,如果主构造函数的所有的参数都有默认值,编译器会生成一个额外的无参构造函数,它将使用默认值。这使得Kotlin 更易于使用像 Jackson 或者 JPA 这样的通过无参构造函数创建类的实例的库。 ``` class Customer(val customerName: String = "") ``` 由此可见:当类存在主构造函数并且有默认值时,次构造函数也适用。 * **示例** ~~~ fun main(args: Array<String>) { println("--------构造函数使用默认值--------") var test = Test() println("--------构造函数使用柱构造函数--------") var test1 = Test(1, 2) println("--------构造函数使用次构造函数--------") var test2 = Test(4, 5, 6) } class Test constructor(num1: Int = 10, num2: Int = 20) { init { println("num1 = $num1\t num2 = $num2") } constructor(num1: Int = 1, num2: Int = 2, num3: Int = 3) : this(num1, num2) { println("num1 = $num1\t num2 = $num2 \t num3 = $num3") } } ~~~ 运行结果 ``` --------构造函数使用默认值-------- num1 = 10 num2 = 20 --------构造函数使用柱构造函数-------- num1 = 1 num2 = 2 --------构造函数使用次构造函数-------- num1 = 4 num2 = 5 num1 = 4 num2 = 5 num3 = 6 ``` ### 创建类的实例 要创建一个类的实例,我们就像调用普通函数一样调用构造函数: ```kotlin val invoice = Invoice() val customer = Customer("Joe Smith") ``` >[warning]注意: Kotlin 并没有 *new*关键字。 创建嵌套类、内部类与匿名内部类的类实例在[嵌套类](http://www.kotlincn.net/docs/reference/nested-classes.html)中有述。 ### 类成员 类可以包含: * [构造函数与初始化块](http://www.kotlincn.net/docs/reference/classes.html#%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0) * [函数](http://www.kotlincn.net/docs/reference/functions.html) * [属性](http://www.kotlincn.net/docs/reference/properties.html) * [嵌套类与内部类](http://www.kotlincn.net/docs/reference/nested-classes.html) * [对象声明](http://www.kotlincn.net/docs/reference/object-declarations.html) ## 继承 在 Kotlin 中所有类都有一个共同的超类 `Any`,这对于没有超类型声明的类而言是默认超类,可参考[根类型“Any”](https://www.kancloud.cn/alex_wsc/android_kotlin/1046286): ```kotlin class Example // 从 Any 隐式继承 ``` `Any` 有三个方法:`equals()`、 `hashCode()` 与 `toString()`。因此,所有 Kotlin 类都定义了这些方法。 如需声明一个显式的超类型(open表示允许其它类继承,和Java中final相反,**默认情况下,Kotlin所有类都是final**),请在类头中把超类型放到冒号之后: ```kotlin open class Base(p: Int) class Derived(p: Int) : Base(p) ``` ### 继承类的构造函数 如果派生类有一个主构造函数,其基类型可以(并且必须)用基类的主构造函数参数就地初始化。 如果派生类没有主构造函数,那么每个次构造函数必须使用 *super* 关键字初始化其基类型,或委托给另一个构造函数做到这一点。 >[info]注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数: ```kotlin class MyView : View { constructor(ctx: Context) : super(ctx) constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) } ``` #### 无主构造函数 如果派生类没有主构造函数,那么每个次构造函数必须使用 *super* 关键字初始化其基类型,或委托给另一个构造函数做到这一点。 例:这里举例在`Android`中常见的自定义View实现,我们熟知,当我们指定一个组件是,一般实现继承类(基类型)的三个构造函数。 ``` class MyView : View(){ constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) } ``` 可以看出,当实现类无主构造函数时,分别使用了`super()`去实现了基类的三个构造函数。 #### 存在主构造函数 如果派生类有一个主构造函数,其基类型可以(并且必须)用基类的主构造函数参数就地初始化。 当存在主构造函数时,主构造函数一般实现基类型中参数最多的构造函数,参数少的构造函数则用`this`关键字引用即可了 ``` class MyView(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : View(context, attrs, defStyleAttr) { constructor(context: Context?) : this(context,null,0) constructor(context: Context?,attrs: AttributeSet?) : this(context,attrs,0) } ``` ### 覆盖方法 我们之前提到过,Kotlin 力求清晰显式。因此,Kotlin 对于可覆盖的成员(我们称之为*open*)以及覆盖后的成员需要**显式修饰符(open)**: ```kotlin open class Shape { open fun draw() { /*……*/ } fun fill() { /*……*/ } } class Circle() : Shape() { override fun draw() { /*……*/ } } ``` `Circle.draw()` 函数上必须加上 *override* 修饰符。如果没写,编译器将会报错。如果函数没有标注 *open*如`Shape.fill()`,那么子类中不允许定义相同签名的函数,不论加不加 **override**。将 *open*修饰符添加到 final 类(即没有 *open*的类)的成员上不起作用。 标记为 *override*的成员本身是开放的,也就是说,它可以在子类中覆盖。如果你想禁止再次覆盖,使用 *final* 关键字: ```kotlin open class Rectangle() : Shape() { final override fun draw() { /*……*/ } } ``` 1. 当基类中的函数,没有用`open`修饰符修饰的时候,实现类中出现的函数的函数名不能与基类中没有用`open`修饰符修饰的函数的函数名相同,不管实现类中的该函数有无`override`修饰符修饰。 ``` open class Demo{ fun test(){} // 注意,这个函数没有用open修饰符修饰 } class DemoTest : Demo(){ // 这里声明一个和基类型无open修饰符修饰的函数,且函数名一致的函数 // fun test(){} 编辑器直接报红,根本无法运行程序 // override fun test(){} 同样报红 } ``` 当一个类不是用`open`修饰符修饰时,这个类默认是`final`的,`class A{}`等价于`final class A{}` 注意,则的`final`修饰符在编辑器中是灰色的,因为Kotlin中默认的类默认是final的 2. 标记为 *override*的成员本身是开放的,也就是说,它可以在子类中覆盖。如果你想禁止再次被覆盖,使用 *final* 关键字。 ### 覆盖属性 属性覆盖与方法覆盖类似;在超类中声明然后在派生类中重新声明的属性必须以 *override*开头,并且父类和子类它们必须具有兼容的数据类型。每个声明的属性可以由具有初始化器的属性或者具有 `get` 方法的属性覆盖。 ```kotlin open class Shape { open val vertexCount: Int = 0 } class Rectangle : Shape() { override val vertexCount = 4//覆盖了vertexCount,赋予它新的数值4 } ``` * 你也可以用一个 `var` 属性覆盖一个 `val` 属性,但反之则不行。这是允许的,因为一个 `val` 属性本质上声明了一个 `get` 方法,而将其覆盖为 `var` 只是在子类中额外声明一个 `set` 方法。 * 请注意,你可以在主构造函数中使用 *override*关键字作为属性声明的一部分。 ```kotlin interface Shape { val vertexCount: Int } class Rectangle(override val vertexCount: Int = 4) : Shape // 总是有 4 个顶点 class Polygon : Shape { override var vertexCount: Int = 0 // 以后可以设置为任何数 } ``` * **Getter()函数慎用super关键字** ~~~ open class Demo { open val valStr = "我是用val修饰的属性" } class DemoTest : Demo() { /* * 这里介绍重写属性是,getter()函数中使用`super`关键字的情况 */ override var valStr: String = "abc" get() = super.valStr set(value) { field = value } } fun main(arge: Array<String>) { println(DemoTest().valStr) val demo = DemoTest() demo.valStr = "1212121212" println(demo.valStr) } ~~~ 运行结果 ``` 我是用val修饰的属性 我是用val修饰的属性 ``` 从上面的执行结果,得出:在实际的项目中在重写属性的时候不用`get() = super.xxx`,因为这样的话,不管你是否重新为该属性赋了新值,它还是支持`setter()`,在使用的时候都调用的是基类中的属性值。 * **在主构造函数中重写** ``` open class Demo { open val num = 0 open val valStr = "我是用val修饰的属性" } class DemoTest2(override var num: Int, override val valStr: String) : Demo() fun main(args: Array<String>) { val demo2 = DemoTest2(1, "构造函数中重写") println("num = ${demo2.num} \t valStr = ${demo2.valStr}") } ``` 运行结果 ``` num = 1 valStr = 构造函数中重写 ``` ### 派生类(子类)初始化顺序 在构造派生类的新实例的过程中,第一步完成其基类的初始化(在此之前只有对基类构造函数参数的检测),因此发生在派生类的初始化逻辑运行之前。 ~~~ //sampleStart open class Base(val name: String) { init { println("Initializing Base")//二 } open val size: Int = name.length.also { println("Initializing size in Base: $it") }//三 } class Derived( name: String, val lastName: String ) : Base(name.capitalize().also { println("Argument for Base: $it") }) {//一 init { println("Initializing Derived")//四 } override val size: Int = (super.size + lastName.length).also { println("Initializing size in Derived: $it") }//五 } //sampleEnd fun main() { println("Constructing Derived(\"hello\", \"world\")") val d = Derived("hello", "world") } ~~~ 执行结果,如上面代码标注的顺序一样 ``` Constructing Derived("hello", "world") Argument for Base: Hello Initializing Base Initializing size in Base: 5 Initializing Derived Initializing size in Derived: 10 ``` 这意味着,基类(父类)构造函数执行时,派生类(子类)中声明或覆盖的属性都还没有初始化。如果在基类初始化逻辑中(直接或通过另一个覆盖的 *open*成员的实现间接)使用了任何一个这种属性,那么都可能导致不正确的行为或运行时故障。设计一个基类时,应该避免在构造函数、属性初始化器以及 *init*块中使用 *open*成员。 ![](https://img.kancloud.cn/13/a8/13a84bd8c99178e4edd685a9ed483911_1257x707.png) ### 调用超类(父类)实现 派生类(子类)中的代码**可以使用 *super*关键字调用其超类的函数与属性访问器的实现**: ```kotlin open class Rectangle { open fun draw() { println("Drawing a rectangle") } val borderColor: String get() = "black" } class FilledRectangle : Rectangle() { override fun draw() { super.draw() println("Filling the rectangle") } val fillColor: String get() = super.borderColor } ``` 在一个内部类中访问外部类的超类,可以通过由外部类名限定的 *super*关键字来实现:`super@Outer`: ```kotlin class FilledRectangle: Rectangle() { fun draw() { /* …… */ } val borderColor: String get() = "black" inner class Filler { fun fill() { /* …… */ } fun drawAndFill() { super@FilledRectangle.draw() // 调用 Rectangle 的 draw() 实现 fill() println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // 使用 Rectangle 所实现的 borderColor 的 get() } } } ``` ### 覆盖规则 在 Kotlin 中,实现继承由下述规则规定:如果一个类从它的直接超类(父类)继承相同成员的多个实现,它必须覆盖这个成员并提供其自己的实现(也许用继承来的其中之一)。 为了表示采用从哪个超类型继承的实现,我们使用由尖括号中超类型名限定的 *super*,如 `super<Base>`: ```kotlin open class Rectangle { open fun draw() { /* …… */ } } interface Polygon { fun draw() { /* …… */ } // 接口成员默认就是“open”的 } class Square() : Rectangle(), Polygon { // 编译器要求覆盖 draw(): override fun draw() { super<Rectangle>.draw() // 调用 Rectangle.draw() super<Polygon>.draw() // 调用 Polygon.draw() } } ``` 可以同时继承 `Rectangle` 与实现接口 `Polygon`,但是二者都有各自的 `draw()` 实现,所以我们必须在 `Square` 中覆盖 `draw()`,并提供其自身的实现以消除歧义。 示例 ~~~ open class A { open fun test1() { println("基类A中的函数test1()") } open fun test2() { println("基类A中的函数test2()") } } interface B { fun test1() { println("接口类B中的函数test1()") } fun test2() { println("接口类B中的函数test2()") } } class C : A(), B { override fun test1() { super<A>.test1() super<B>.test1() } override fun test2() { super<A>.test2() super<B>.test2() } } fun main(args: Array<String>) { val C = C() C.test1() C.test2() } ~~~ 运行结果 ``` 基类A中的函数test1() 接口类B中的函数test1() 基类A中的函数test2() 接口类B中的函数test2() ``` ## 抽象类 类以及其中的某些成员可以声明为 *abstract*。抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用 `open` 标注一个抽象类或者函数——因为这不言而喻。 我们可以用一个抽象成员覆盖一个非抽象的开放成员 ```kotlin open class Polygon { open fun draw() {} } abstract class Rectangle : Polygon() { override abstract fun draw() } ``` ## 伴生对象 如果你需要写一个可以无需用一个类的实例来调用、但需要访问类内部的函数(例如,工厂方法),你可以把它写成该类内[对象声明](http://www.kotlincn.net/docs/reference/object-declarations.html)中的一员。 更具体地讲,如果在你的类内声明了一个[伴生对象](http://www.kotlincn.net/docs/reference/object-declarations.html#%E4%BC%B4%E7%94%9F%E5%AF%B9%E8%B1%A1),你就可以调用其成员,只是以类名作为限定符。