类
继承和重写
kt 中使用 :(冒号) 代替 java 中的 extends 和 implements
重写父类的方法时,必须使用 override 标识。
当有多个父类,且父类中的方法冲突时,可以通过 把父类放到 super<> 的尖括号中指定调用哪个父类中的方法。
class Impl:Itf,Itf2{
override fun test() {
super<Itf>.test() // 调用 Itf 接口中的 test 方法
super<Itf2>.test() // 调用 Itf 接口中的 test 方法
super.say() // 方法不冲突时,直接使用 super. 调用父类的方法
println("impl")
}
}
类的定义
使用 class 关键字定义一个类
class Demo2{
fun test() = println("demo2#test")
fun test2() = println("demo2#open")
}
kt 中的所有类都是 final ,不能被继承;所有方法也都是 final 不能被重写。
open 与 final
-
kt 中,类、方法默认都是 final。
-
可以使用 open 修饰符取消类、方法或属性的 final 属性。
-
继承的方法、属性也是 open,除非手动使用 final 进行修饰:
class Demo
open class Demo2{
fun test() = println("demo2#test")
open fun test2() = println("demo2#open")
}
open class Demo3:Demo2(){ // 可以继成 Demo2,但不能集成 Demo
final override fun test2() { // 重写 test2() ,但不能重写 test
super.test2()
}
}
class Demo4:Demo3(){
// 由于 Demo3 将 test2() 声明为 final 类型,所以该类中没有可重写的方法
}
构造函数
kt 中的构造函数分为 主构造函数,从构造函数。主构造函数在类的外部声明,而从构造函数在类的内部声明。
- 所有的从构造函数使用 constructor 声明。
如果没有给一个类声明任何构造函数,将会生成一个不做任何事情的默认构造函数。
-
同时定义主构造函数与从构造函数时,从构造函数必须调用到主构造函数。如下,虽然主构造函数没有做任何操作,但从构造函数最终必须要调用到主构造函数。
-
构造函数通过 this 相互调用。
class Child2(){
constructor(a:Int):this(a,1)
constructor(a:Int,b:Int):this()
}
主构造函数与 init
主构造函数定义在类的外面。
主构造函数功能很单一,只能 为类定义和参数同名的属性,并生成其对应的 getter/setter。如果需要在构造函数中执行一些初始化操作,需要使用 init 定义初始化语句块。
-
初始化语句块只能与主构造函数一起使用。一个类中可以定义多个初始化语句块。
-
init 块中可以使用主构造函数中的参数。如下面代码中,可以在 init 块中直接使用 name 参数。
// 定义主构造函数
class Demo constructor(name:String){
val name:String
init {
this.name = name
}
}
上述代码如下面的 java 代码功能一样,为类定义一个 name 属性,同时将构造函数中的 name 值赋值给 name 属性:
class Demo{
public final String name;
Demo(String name){
this.name = name;
}
}
主构造函数的演化
在不进行简化的情况下,主构造函数的定义如下:
// 定义构造函数,同时对主构造函数进行修饰与使用注解
class Test @A private constructor(name:String){
lateinit var name_:String
init {
this.name_ = name
}
}
-
如果主构造函数没有被注解或者可见性修饰符修饰时,可以直接使用 constructor。如下:
class Test constructor(name:String){ lateinit var name_:String init { this.name_ = name } }
-
当主构造函数只有 constructor 时,可以省略 constructor 关键字。因此上面代码可以简写如下:
class Test(name:String){ lateinit var name_:String init { this.name_ = name } }
-
如果 init 代码块中只是为同名属性赋值,可以省略 init 代码块:
class Test(name:String){ val name = name // 可以直接使用构造函数中的参数为属性赋值 }
-
如果不需要为属性自定义 getter/setter,可以将修饰属性的 var/val 提到构造函数中。此时会生成默认的 getter/setter
class Test(val name: String)
-
主构造函数中的属性无法直接设置 getter/setter,需要用另外的属性:
class Test(name:String,age:Int){ var name = name get() { println("getter") return "from getter" } }
从构造函数
定义在类的内部,而不是类名后面的构造函数
大部分情况下,使用主构造函数就够了。之所以定义从构造函数,是为了能通过不同的方式初始化类。
```kotlin
class Demo{
val name:String
// 从构造函数
constructor(name: String){
this.name = name
}
}
```
继承时的构造函数
同 java 一样,子类的构造函数必须调用到父类的构造函数。
-
唯一原则:确保无论用户调用哪个构造函数,它必须能调用到父类的构造函数。
-
有主构造函数时,从构造函数不能直接调用父构造函数。
open class Parent{ constructor(name: String) } class Child():Parent("x") class Child2:Parent{ constructor():super("xx") } class Child3():Parent(""){ constructor(name:String):this() }
第一个类中,用户调用主构造函数时,可以调用到父类。
第二个类中,用户调用从时,可以调用到父类。如果定义主构造函数,则会报错。因为用户调用主构造函数时,无法调用到父类的构造函数。
第三个类中,有主从。从只能调用主构造函数。这是因为主构造函数可能会定义一系列的变量,如果调用不到主,则这些变量无法初始化。
抽象类
定义方式与 java 一样。
-
普通类默认是 final ,但抽象类始终是 open
-
抽象类中的非抽象函数默认是 final,抽象函数是 open。
abstract class Demo2{
fun test() = println("demo2#test") // 默认是 final,子类不能重写 test()
open fun test2() = println("demo2#open") // 显式使用 open 关键字
abstract fun test3() // 抽象函数,默认是 open。被重写后,默认也是 open
}
内部类与嵌套类
-
kt 中直接将一个类定义在另一个类内部,该类叫嵌套类。嵌套类不持有外部类的引用。相当于 java 中被 static 修饰的内部类。
-
嵌套类使用 inner 修饰,则该类为内部类,此时同普通的 java 内部类。可以引用外部类的成员属性、方法。
-
内部类通过 this@Outer 获取外部类的引用
class Student{ val name = "name" override fun toString(): String { return "toString" } class Inner{ fun test() = println("--") // 无法引用外部类 } // 定义成内部类 inner class InnerClass{ override fun toString(): String { return "innerclass" } fun test() = println("$name ${this@Student.toString()}") // name toString } }
密封类
使用 sealed 修饰符定义的类是密封类,密封类要求 所有直接子类必须定义在父类所在的文件中。
-
sealed 隐含的这个类是 open 类,不需要再显式地声明 open。
-
密封类的构造方法是 private 。
-
java 中不能继承密封类。kt 中可以在密封类所在的文件中定义密封类的子类,不是同一文件不继承。
-
sealed 不能用于修饰接口。
// 密封类
sealed class Student{
// 内部类可以继承
private class S : Student()
}
// 同文件中可以继承
class S1:Student()
数据类
使用
data
修饰的类为数据类,数据类会根据主构造函数自动重写能用方法
通用方法一般包括有:equals(),toString(), hashCode()。
-
equals() 会检测所有属性的值是否相等
-
toString() 生成按声明顺序排列的所有字段的字符串表达形式
-
hashCode() 会返回一个根据所有属性生成的哈希值。
-
equals ,toString() 和 hashCode() 只会考虑在主构造函数中声明的属性。
-
由于 hashCode 会随属性值的变化而变化,因此在用于 map 中作为键值时,其 主构造函数中的属性必须要声明为 val。
fun main(args:Array<String>){ val demo = Parent(1,"ls") println("hashCode = ${demo.hashCode()} toString = ${demo.toString()}") val d = Parent(1,"ls",20) println("hashCode = ${d.hashCode()} toString = ${d.toString()}") println(demo == demo) // true val map = hashMapOf<Parent,String>() val key = Parent(1,"ss") map.put(key,"000") println(map[key]) // 000 key.name = "xx" println(map[key]) // null } data class Parent(var age:Int,var name:String){ var score = 0 constructor(age:Int,name: String,score:Int):this(age,name){ this.score = score } }
上述代码中,如果修改其中一个属性的值, map 返回的结果为 null。这是因为 hashCode 值已经发生了变化。
-
data 类在生成时,会自动添加 copy 方法:在调用 copy() 时修改其些属性的值。避免无法修改属性值。这就是原型模式。
fun main(args:Array<String>){ val demo = Parent(1,"ls") val dc = demo.copy(2,"copy") // 两者的 hashCode 返回结果不一样 println("demo = ${demo.hashCode()} dc = ${dc.hashCode()}") }
类委托
在使用装饰模式时经常会出现这种情况:除了少部分要重写的方法外,大部分方法都只是简单的将请求转发给被装饰者。这些转发请求的代码就是所谓的样板代码 —— 所有的转发代码格式都一样。
kt 中使用 by 关键字省略样板代码。
如下面代码,除了 add 方法外,其余的方法都是直接将操作转发给 innerSet 的同名操作。
// 使用 by 关键字常未重写的操作委托给 innerSet ,而重写的方法调用 Count 定义的
class Count<T>(private val innerSet:MutableCollection<T> = HashSet()):MutableCollection<T> by innerSet{
override fun add(element: T): Boolean {
println("add ${element}")
return innerSet.add(element)
}
}