扩展
Kotlin 的扩展是一个很独特的功能, Java本身并不支持扩展, Kotlin 为了让扩展能在JVM平台上运行,必须做一些独特的处理 。 本节将会详细介绍这些处理 。
Kotlin 支持扩展方法和扩展属性 。
扩展方法
扩展方法的语法很简单,其实就是定义一个函数,只是函数名不要写成简单的函数,而是要在函数名前增加被扩展的类(或接口)名和点号(.) 。 例如如下程序
open class Raw{
fun test(){
println("test()方法")
}
}
class RawSub :Raw(){
fun sub(){
println("--sub()方法--")
}
}
//定义一个函数 ,函数名为“被扩展类.方法”
fun Raw.info(){
println("--扩展的info()方法--")
}
fun main(args: Array) {
var r =Raw()
r.test()
//调用 Raw 对象扩展的方法
r.info()
//创建 Raw 类的子类的实例
var rsub = RawSub()
rsub.sub()
rsub.test()
//Raw 的子类的对象也可调用扩展的方法
rsub.info()
}
上面程序定义了 一个 Raw 类,该 Raw 类使用了 open 修饰符,这是为了示范给 Raw 类派生子类 。 如果只是对 Raw 类进行扩展,那么 Kotlin 并不需要使用 open 修饰该类 。
Raw 类扩展了 info()方法之后,就像为 Raw 类增加了 info()方法一样,所有的 Raw 对象都可调用 info()方法。不仅如此,Raw类的子类的实例也可调用 info方法。就像真的为Raw类增加了一个方法一样。扩展方法中的 this 与成员方法中的 this 一样 ,也代表调用该方法的对象 。
扩展也可以为系统的类增加方法,下面的程序将为系统的List集合拓展一个shuffle()方法,该方法用于对集合元素进行随机排列 。
import java.util.*
//该方法的实现思路是:先生成 List 集合所有索引的随机排列
// 然后根据随机排列的索引去 List 集合中取元素
fun List.shuffle(): List {
val size = this.size
//下面的 indexArr 用于保存 List 集合的索引的随机排列
var indexArr = Array(size, { 0 })
var result: MutableList = mutableListOf()
//创建随机对象
val rand = Random()
var i = 0
outer@ while (i < size) {
//生成随机数
var r = rand.nextInt(size)
for (j in 0 until i) {
//如果 r 和前面已生成的任意数字相等,则该随机数不可用,需要重新生成
if(r == indexArr[j]){
continue@outer
}
}
//如果上面的循环结束了都没有执行 continue,则说明该 r 是一个不重复的随机数
//将随机数 r 存入 indexArr 数组中
indexArr[i] = r
//根据随机的索引读取 List 集合元素,并将元素添加到 result 集合中
result.add(this[r])
i++
}
return result.toList()
}
fun main(args: Array) {
var nums = listOf(20,19,23,1,23,64)
//调用程序为 List 扩展的 shuffle ()方法
println(nums.shuffle())
println(nums.shuffle())
}
上面程序扩展List时指定了泛型参数为Int,因此该扩展只对List
实际上 Kotlin 完全支持直接进行泛型类扩展,只不过此时需要在函数上使用泛型,因此要使用泛型函数的语法。
//该方法的实现思路是:先生成 List 集合所有索引的随机排列
// 然后根据随机排列的索引去 List 集合中取元素
fun List.shuffle(): List {
val size = this.size
//下面的 indexArr 用于保存 List 集合的索引的随机排列
var indexArr = Array(size, { 0 })
var result: MutableList = mutableListOf()
//创建随机对象
val rand = Random()
var i = 0
outer@ while (i < size) {
//生成随机数
var r = rand.nextInt(size)
for (j in 0 until i) {
//如果 r 和前面已生成的任意数字相等,则该随机数不可用,需要重新生成
if(r == indexArr[j]){
continue@outer
}
}
//如果上面的循环结束了都没有执行 continue,则说明该 r 是一个不重复的随机数
//将随机数 r 存入 indexArr 数组中
indexArr[i] = r
//根据随机的索引读取 List 集合元素,并将元素添加到 result 集合中
result.add(this[r])
i++
}
return result.toList()
}
fun main(args: Array) {
var strList = listOf("ss","111","nnn","dd","vv")
//调用程序为 List 扩展的 shuffle ()方法
println(strList.shuffle())
println(strList.shuffle())
}
扩展的实现机制
我们知道, Java是一门静态语言。一个类被定义完成之后,程序无法动态地为该类增加、 删除成员( field、方法等),除非开发者重新编辑该类的源代码,并重新编译该类。
但现在 Kotlin 的扩展却好像可以动态地为一个类增加新的方法,而且不需要重新修改该类的源代码,这真是太神奇了!那 Kotlin 扩展的实际情况是怎样的呢?难道 Kotlin 可以突破JVM的限制?
实际上, Kotlin的扩展并没有真正地修改所扩展的类,被扩展的类还是原来的类,没有任何改变。 Kotlin扩展的本质就是定义了一个函数,当程序用对象调用扩展方法时, Kotlin在编译时会执行静态解析一一就是根据调用对象、方法名找到扩展函数,转换为函数调用。例如:
strList.shuffle()
Kotlin 在编译时这行代码按如下步骤执行:
- 检查 strList 的类型,发现其类型是 List
。 - 检查 List
类本身是否定义了 shuffle()方法,如果该类本身包含该方法,则 Kotlin
无须进行处理,直接编译即可。- 如果 List
类本身不包含 shuffle()方法,则 Kotlin会查找程序是否为 List 扩展了 shuffle()方法一一也就是查找系统中是否包含了名为 List .shuffile()的函数(或泛型函数)定义,如果找到该函数,则 Kotlin 编译器会执行静态解析,它会将上面代码替换成执行 List .shuffile()函数。 - 如果 List
不包含 shuffle()方法,也找不到名为 List .shuffile()的函数(或泛型函数)定义,编译器将报错。
由此可见, Kotlin 的扩展并没有真正改变被扩展的类, Kotlin 只是耍了一个小“花招”,当 Kotlin 程序调用扩展方法时, Kotlin 编译器会将这行代码静态解析为调用函数,这样 JVM 就可接受了。这意味着调用扩展方法是由其所在表达式的编译时类型决定的,而不是由它所在表达式的运行时类型决定的。
//定义一个 Base 类
open class Base
//定义 Sub 类继承 Base 类
class Sub : Base()
//为 Base 类扩展 foo 方法
fun Base.foo(){
println("Base扩展的 foo()方法")
}
//为 Sub 类扩展 foo 方法
fun Sub.foo() = println("Sub扩展的foo()方法")
//定义一个函数
fun invokeFoo(b:Base){
//调用 Base 对象的 foo ()方法
b.foo()
}
fun main(args: Array) {
//传入的是 Sub对象
//输出Base扩展的 foo()方法
invokeFoo(Sub())
val bb: Base = Sub()
//输出Base扩展的 foo()方法
bb.foo()
}
上面程序中定义了 一个 Base 类及其子类 Sub,接下来程序为 Base 和 Sub 分别扩展了 foo() 方法。
对于有继承关系的 Base和 Sub两个类,它们都包含了具有相同签名的 foo()方法,如果 foo() 方法是成员方法,程序就必须用 override 声明子类方法重写父类方法;但由于此处采用的是扩展, 因此 Kotlin 不需要声明为方法重写。
上面程序中定义了 一个 invokeFoo(Base)函数,该函数的形参类型是Base,但调用该函数时传入一个 Sub对象,那么此处到底是调用 Base类扩展的 foo()方法, 还是调用 Sub类扩展的 foo()方法呢?
如果foo()方法是Base、 Sub所包含的成员方法(方法重写),那么一定是由JVM动态解析为调用运行时类型( Sub 对象)的方法。
但此处的 foo()方法是 Base、 Sub 所包含的扩展方法,对于扩展方法由于 Kotlin 编译器执行静态解析,在编译阶段 Kotlin编译器只知道 invokeFoo()方法的形参是 Base类型,因此 Kotlin 编译器会将其替换为调用 Base 的 foo()方法。
总结起来一句话 : 成员方法执行动态解析(由运行时类型决定);扩展方法执行静态解析(由编译时类型决定)。
此外,前面介绍了 Kotlin 编译时解析一个成员方法和扩展方法的步骤,由此可知成员方法的优先级高于扩展方法。这意味着 ,如果一个类包含了具有相同签名的成员方法和扩展方法,当程序调用这个方法时,系统总是会执行成员方法,而不会执行扩展方法。
为可空类型扩展方法
Kotlin 还允许为可空类型(带“?”后缀的类型)扩展方法。由于可空类型允许接受null 值,这样使得 null值也可调用该扩展方法。从另一方面来看 , 由于会导致null值调用该扩展方法,因此程序需要在扩展方法中处理null值的情形。例如如下程序。
//为可空类型扩展 equals 方法
fun Any?.equals(other: Any?): Boolean {
if (this == null) {
return if (other == null) true else false
}
return this.equals(other)
}
fun main(args: Array) {
var a = null
println(a.equals(null)) //输出 true
println(a.equals("sss")) //输出 false
}
扩展属性
Kotlin也允许扩展属性,但由于 Kotlin 的扩展并不能真正修改目标类,因此 Kotlin扩展的属性其实是通过添加 getter、 setter 方法实现的,没有幕后字段。简单来说,扩展的属性只能是计算属性 !
由于 Kotlin 的扩展属性只能是计算属性,因此对扩展属性有如下三个限制。
- 扩展属性不能有初始值(没有存储属性值的幕后字段) 。
- 不能用 field关键字显式访问幕后字段。
- 扩展只读属性必须提供 getter方法;扩展读写属性必须提供 getter、 setter方法。 如下程序示范了为 Kotlin类添加扩展属性。
class User(var first: String, var last: String)
//为 User 扩展读写属性
var User.fullName: String
get() = "${last}.${first}"
set(value) {
println("执行扩展属性 fullName 的 setter 方法")
//value 字符串中不包含.或包含几个 .都不行
if ("." !in value || value.indexOf(".") != value.lastIndexOf(".")) {
println("您输入的 fullName 不合法")
} else {
var tokens = value.split(".")
first = tokens[1]
last = tokens[0]
}
}
fun main(args: Array) {
var user=User("悟空","孙")
println(user.fullName)
user.fullName="八戒.猪"
println(user.last)
println(user.first)
}
此外,由于扩展属性的本质就是 getter、 setter方法,因此也可用泛型函数的形式来定义扩展属性。例如, Kotlin为 List扩展的 lastIndex属性的代码如下:
val List.lastindex: Int
get() = size - 1
以成员方式定义扩展
前面见到的扩展,都是以顶层函数的形式(放在包空间下)进行定义的,因此这些扩展都可直接使用(如果扩展位于不同的包中,当然也需要导包)。 Kotlin 还支持以类成员的方式定义扩展一一就像为类定义方法、属性那样定义扩展。
对于以类成员方式定义的扩展,一方面它属于被扩展的类,因此在扩展方法(属性〉中可直接调用被扩展类的成员(省略 this 前缀): 另一方面它又位于定义它所在类的类体中 , 因此在扩展方法(属性)中又可直接调用它所在类的成员(省略 this 前缀)。
class A {
fun bar() = println("A的bar方法")
}
class B {
fun baz() = println("B的 baz 方法")
//以成员方式为A扩展 foo()方法
fun A.foo() {
//在该方法内既可调用类 A 的成员,也可调用类 B 的成员
// A对象为隐式调用者
bar()
//B对象为隐式调用者
baz()
}
fun test(target: A) {
//调用 A 对象的成员方法
target.bar()
//调用 A 对象的扩展方法
target.foo()
}
}
fun main(args: Array) {
var b = B()
b.test(A())
}
上面程序在类 B 中为类 A 扩展了一个 foo()方法。由于该foo()方法一方面属于类 A,另一 方面又定义在类 B 中,因此在 foo()方法内既可直接调用类 A 的成员,也可直接调用类 B 的成员,且都不需要使用 this前缀。
这样又会产生一个新的问题。如果被扩展类和扩展定义所在的类包含了同名的方法,此时就会导致:程序在扩展方法中调用两个类都包含的方法时,系统总是优先调用被扩展类的方法。为了让系统调用扩展定义所在类的方法,必须使用带标签的this进行限定。
class Tiget {
fun foo() {
println("Tiger 类的foo()方法")
}
}
class Bear {
fun foo() {
println("Bear 类的foo()方法")
}
//以成员方式为 Tiger 类扩展 test ()方法
fun Tiget.test() {
foo()
//使用带标签的 this 指定调用 Bear 的 foo ()方法
[email protected]()
}
fun info(tiget: Tiget) {
tiget.test()
}
}
fun main(args: Array) {
val b = Bear()
b.info(Tiget())
}
上面程序在 Bear类中为 Tiger类扩展了一个 test()方法,且 Bear类和 Tiger类都包含 foo() 方法,因此如果程序在 test()扩展方法中调用 foo()方法,系统总是优先调用 Tiger类的foo()方 法。为了在 test()扩展方法中调用 Bear定义的 foo()方法,就需要使用 this@Bear前缀,如上面程序所示。
Kotlin 的 this 比 Java 的 this 更强大, Kotlin 的 this 支持用“@类名”形式,这种形式限制了该 this 代表哪个类的对象。
带接收者的匿名函数
Kotlin还支持为类扩展匿名函数,在这种情况下,该扩展函数所属的类也是该函数的接收者。因此,这种匿名函数也被称为“带接收者的匿名函数”。
与普通扩展方法不同的是 :去掉被扩展类的类名和点(.)之后的函数名即可,其他部分并没有太大的区别。与普通扩展方法相似的是,带接收者的匿名函数(相当于扩展匿名函数)也允许在函数体内访问接收者对象的成员。
//定义一个带接收者的匿名函数
val factorial = fun Int.(): Int {
//该匿名函数的接收者是 Int 对象
//因此在该匿名函数中, this代表调用该匿名函数的Int对象
if (this < 0) {
return -1
} else if (this == 1) {
return 1
} else {
var result = 1
for (i in 1..this) {
result *= i
}
return result
}
}
fun main(args: Array) {
println(6.factorial())
}
上面程序中定义了一个带接收者的匿名函数,相当于程序为 Int 扩展了一个匿名函数。如果写成 fun Int.abc(): Int,就表示为 Int类扩展了 abc()方法;但是 fun Int.(): Int,此时 Int后什么也没有,也就是没有指定函数名,因此它是一个匿名函数。
由于上面程序最后将带接收者的匿名函数赋值给了 factorial变量,因此可通过 Int对象来调用 factorial()函数。
与普通函数相似的是,带接收者的匿名函数也有自身的类型,即带接收者的函数类型。例如,上面 factorial变量的类型为:
Int.()->Int
该类型就是在普通函数类型的前面添加了一个接收者类型进行限定。
如果接收者类型可通过上下文推断出来,那么 Kotlin允许使用 Lambda表达式作为带接收者的匿名函数 。 例如如下程序 。
class Html {
fun body() {
println(" ")
}
fun head() {
println(" ")
}
}
//定义一个类型为 Html. ()->Unit 的形参(带接收者的匿名函数)
//这样在函数中 Html 对象就增加了 一个 init 方法
fun html(init: Html.() -> Unit) {
println("")
val html = Html() //创建接收者对象
html.init() //使用接收者调用 init 引用匿名函数(即传入的参数)
println("")
}
fun main(args: Array) {
//调用 html 函数,需要传入 Html.()-> Unit 类型的参数
//此时系统可推断出接收者的类型,故可用 Lambda 表达式代替匿名函数
html {
//Lambda 表达式中的 this 就是该方法的调用者
head()
body()
}
}
上面程序先定义了 一个Html 类,并在该类中定义了 body()和 head()两个方法,这个类没有任何特别的地方 。
接下来程序定义了一个 html()函数,该函数的形参是 HTML.()->Unit 类型,即带接收者的函数类型 。 html()函数的形参名为init,这意味着当程序调用html()函数时传入的匿名函数(或 Lambda 表达式)会传给该 init 参数,这样在该 html()函数内, HTML 对象就被动态扩展了一个 init()方法,而且该方法的执行体是动态传入的 。
在 main()函数中调用 html()函数,调用语法看上去有点奇怪,这一点在以前有介绍:如果调用函数只有一个 Lambda 表达式参数,则可以省略调用函数的圆括号,将Lambda 表达式放在函数之外即可。也就是说,main()函数中代码的完整形式如下:
html({
//Lambda 表达式中的 this 就是该方法的调用者
head()
body()
})
何时使用扩展
扩展无非就是为类增加方法或属性, 为什么不直接在类中定义这些方法和属性,还要通过扩展来定义呢?这不是多此 一举吗?
多现代编程语言都已支持扩展,由此可见扩展的魅力 。
扩展的作用主要有如下两个方面 。
扩展可动态地为已有的类添加方法或属性 。
扩展能以更好的形式组织一些工具方法 。
关于上面第一点,有时候我们要为一些已有框架或库的类增加额外的功能,如果使用 Java,则只能通过派生子类来实现,但一方面派生子类有一些限制,另一方面派生子类的性能开销也比较大。而扩展的出现很好地解决了这个问题 。
扩展是一种非常灵活的动态机制,它既不需要使用继承,也不需要使用类似于装饰者的任何设计模式,即可为现有的类增加功能,因此使用非常方便。
关于上面第二点,我们知道Java 系统提供了 Arrays、 Collections、 Files 等各种类,还有第三方库提供的大量的 StringUtils、 BeanUtils等类,这些类的作用非常明确:工具类,包含了操作特定类型的工具方法 。 比如 Arrays 包含了大量操作数组的工具方法, Collections 包含 了大量操作集合的工具方法 。 例如要使用 Collections对 List集合元素排序,则需要使用如下代码:
Collections.sort(list)
上面这行代码平白无故地多出一个 Collections 工具类,而且这也不符合面向对象的代码风格(面向对象的代码风格是:猪八戒.吃(西瓜)),这里多出来的 Collections类真让人尴尬。
其实希望使用如下更简洁的代码:
list. sort ()
此时就需要让 List 集合本身具有 Collections 类提供的工具方法,通过扩展即可为 List 集合增加这些工具方法。
final和 open修饰符
final 关键字可用于修饰类、属性和方法,表示它修饰的类、属性和方法不可改变 。
Kotlin 有一个非常特别的设计:它会为非抽象类自动添加 final 修饰符,也会为非抽象方法、非抽象属性等无须重写的成员自动添加final修饰符。如果开发者希望取消Kotlin自动添加final修饰符,则可使用 open 修饰符, open 修饰符与final 修饰符是反义词。
此外, Kotlin 与 Java 的一个重大区别是: final 修饰符不能修饰局部变量,因此open 自然也不能修饰局部变量 。
可执行“宏替换”的常量
java使用 final 修饰“宏变量”,该“宏变量”在编译阶段就会被替换掉 。
Kotlin提供了const用来修饰可执行“宏替换”的常量,这种常量也被称为“编译时”常量 ,因为它在编译阶段就会被替换掉 。
“宏替换”的常量除使用 const修饰之外,还必须满足如下条件。
- 位于顶层或者是对象表达式的成员。
- 初始值为基本类型值 (Java 的 8 种基本类型)或字符串字面值。
- 没有自定义的 getter方法。
//定义支持 “宏替换”的常量
const val MAX_VALUE =100
fun main(args: Array) {
println(MAX_VALUE)
}
上面程序中使用 const定义了一个支持“宏替换”的常量,并在定义该常量 时指定初始值为 100。对于这个程序来说,常量 MAX AGE 其实根本不存在,当程序执行 println(MAX_AGE)代码时,实际替换为执行 println(l00),而且这个替换在编译阶段就完成 了,因此程序在运行阶段完全没有 MAX_AGE 常量。
此外,如果被赋值的表达式只是基本的算术表达式或进行字符串连接运算,没有访问普通变量、常量,调用方法,那么 Kotlin编译器同样会将这种 const常量当成“宏变量”处理。
final 属性
final属性表明该属性不能被重写,而且如果程序对属性不使用任何修饰符, Kotlin会自动为该属性添加final修饰。
final 方法
使用 final 修饰的方法不可被重写。与属性设计类似的是,如果程序不为方法添加任何修饰符, Kotlin 会自动为该方法添加 final 修饰。
final 类
使用 final 修饰的类不可以有子类,与方法、属性的设计相同:如果一个类没有显式使用 open修饰符修饰,那么 Kotlin会自动为该类添加 final修饰。
不可变类
不可变( immutable)类的意思是创建该类的实例后,该实例的属性值是不可改变的 。
如果需要创建自定义的不可变类,可遵守如下规则。
- 提供带参数的构造器,用于根据传入的参数来初始化类中的属性。
- 定义使用 final修饰的只读属性,避免程序通过 setter方法改变该属性值。
如果有必要,则重写Any类的 hashCode()和 equals()方法。 equals()方法将关键属性作为两个对象是否相等的标准。除此之外,还应该保证两个用 equals()方法判断为相等的对象的hashCode()也相等 。
例如,String这个类就做得很好,它就是将String对象中的字符序列作为相等的标准,其hashCode()方法也是根据字符序列计算得到的。
下面定义一个不可变的 Address 类,程序把 Address 类的 detail 和 postCode 属性都定义成只读属性,并使用 final修饰这两个属性,不允许其他方法修改这两个属性的值。
//定义可初始化两个属性的构造器
class Adress(val detail: String, val postCode: String) {
override fun equals(other: Any?): Boolean {
if (this == other) {
return true
}
if (other == null) {
return false
}
if (other.javaClass == Adress::class) {
var ad = other as Adress
//当 detail 和 postcode 相等时,可认为两个 Address 对象相等
return this.detail.equals(ad.detail) && this.postCode.equals(ad.postCode)
}
return false
}
override fun hashCode(): Int {
return detail.hashCode() + postCode.hashCode()*31
}
}
对于上面的 Address类,当程序创建了 Address对象后,同样无法修改该 Address对象的detail 和 postCode 属性 。
与不可变类对应的是可变类,其含义是该类的对象的属性值是可变的。大部分时候所创建的类都是可变类,只要我们定义了任何读写属性,该类就是可变类。
与可变类相比,不可变类的实例在整个生命周期中永远处于初始化状态,它的属性值不可改变。因此,对不可变类的对象的控制将更加简单。
有一个问题需要说明 : 当创建不可变类时,如果它包含的成员属性的类型是可变的,那么其对象的属性值依然是可改变的,这个不可变类其实是失败的。
下面程序试图定义一个不可变的Person类,但因为Person 类包含一个可变类型的属性,所以导致Person类也变成了可变类。
class Name(var firstName: String = "", var lastName: String = "") {
}
class Person(val name: Name){
}
fun main(args: Array) {
var name = Name("悟空","孙")
var p = Person(name)
//输出悟空
println(p.name.firstName)
//改变 Person 对象的 name 的 firstName 值
p.name.firstName="八戒"
//Person 对象的 name 的 firstName 值被改为 ”八戒 ”
println(p.name.firstName)
}
上面程序中代码修改了 Name 对象(可变类的实例)的firstName 的值,但由于 Person 类的 name属性引用了该Name对象,就会导致Person对象的 name的 firstName的值会被改变,这就破坏了设计 Person类的初衷。
为了保持 Person对象的不可变性,必须保护好 Person对象的引用类型的属性: name,让程序无法访问到 Person对象的 name 属性的幕后变量,也就无法利用 name 属性的可变性来改变Person对象了。为此将 Person 类改为如下形式:
class Name(var firstName: String = "", var lastName: String = "") {
}
class Person{
val name:Name
//返回一个新的对象,该对象的 firstName 和 lastName
//与该 Person 对象里的幕后字段的 firstNarne 和 lastName 相同
get() = Name(field.firstName,field.lastName)
constructor(name:Name){
//设置 name 属性值为新创建的 Name 对象,该对象的 firstName 和 lastName
//与传入的 name 参数的 firstName 和 lastName 相同
this.name = Name(name.firstName,name.lastName)
}
}
注意阅读上面代码, Person 类改写了设置 name 属性的方法,也改写了name属性的 getter方法。当程序向 Person构造器里传入一个 Name对象时,该构造器创建 Person 对象时并不是直接利用已有的 Name 对象(利用已有的 Name 对象有风险,因为这个已有的 Name 对象是可变的,如果程序改变了这个 Name 对象,将会导致 Person 对象也发生变化), 而是重新创建了一个 Name 对象来赋给 Person 对象的 name 属性。当 Person 对象返回 name 属性时,它并没有直接返回 name 属性的幕后字段,因为直接返回 name 属性的幕后字段也可能导致它所引用的 Name 对象被修改。
如果将 Person 类定义改为上面形式, Person 对象的 name 的 firstName 不会被修改。
因此,如果需要设计一个不可变类,尤其要注意其引用类型的属性,如果属性的类型本身是可变的,就必须采取必要的措施来保护该属性所引用的对象不会被修改,这样才能创建真正的不可变类。
抽象类
当编写一个类时 ,常常会为该类定义一些方法 ,这些方法用以描述该类的行为方式,这些方法都有具体的方法体。但在某些情况下,某个父类只是知道其子类应该包含怎样的方法,但无法准确地知道这些子类如何实现方法。例如定义一个 Shape类,这个类应该提供一个计算周长的方法 calPerimeter(),但不同的Shape子类对周长的计算方法是不一样的,即 Shape类无法准确地知道其子类计算周长的方法。
可能有人会说,既然Shape类不知道如何实现calPerimeter()方法,那就干脆不要管它了!这不是一个好思路:假设有一个 Shape变量,该变量实际上引用到 Shape子类的实例,那么这个Shape 变量就无法调用 calPerimeter()方法(必须将其强制转换为其子类类型,才可调用calPerimeter()方法),这就降低了程序的灵活性 。
那么如何既能让 Shape 类中包含 caIPerimeter()方法,又无须提供其方法实现呢?使用抽象方法即可满足该要求:抽象方法是只有方法签名,没有方法实现的方法。
需要说明的是,有abstract修饰的成员,无须使用 open 修饰,当使用 abstract 修饰类时,表明这个类需要被继承:当使用 abstract修饰方法、属性时,表明这个方法、属性必须由子类提供实现(即重写)。而使用 final 修饰的类不能被继承,使用 final 修饰的方法不能被重写。 因此 final 和 abstract 永远不能同时使用。
但是,抽象类中的具体方法、属性依然有 final 修饰,如果程序需要重写抽象类中的具体方法、属性,则依然需要显式为这些方法、属性添加 open 修饰。
抽象成员和抽象类
抽象成员(方法和属性)和抽象类必须使用 abstract修饰符来定义,包含抽象成员的类只能被定义成抽象类,抽象类中可以没有抽象成员。
抽象方法和抽象类的规则如下。
- 抽象类必须使用 abstract 修饰符来修饰,抽象成员也必须使用 abstract 修饰符来修饰,抽象方法不能有方法体。
- 抽象类不能被实例化,无法调用抽象类的构造器创建抽象类的实例。即使抽象类中不包含任何抽象成员,这个抽象类也不能创建实例。
- 抽象类可以包含属性、方法(普通方法和抽象方法都可以)、构造器、初始化块、嵌套类(接口、枚举) 5 种成员 。 抽象类的构造器不能用于创建实例,主要用于被其子类调用。
- 含有抽象成员的类(包括直接定义了 一个抽象成员:或继承了一个抽象父类,但没有完全实现父类包含的抽象成员;或实现了一个接口,但没有完全实现接口包含的抽象成员这三种情况)只能被定义成抽象类 。
定义抽象方法,只需在普通方法上增加 abstract修饰符,并把普通方法的方法体(也就是方法后用花括号括起来的部分)全部去掉即可。
定义抽象类,只需在普通类上增加abstract修饰符即可。甚至一个普通类(没有包含抽象方法的类)增加 abstract修饰符后也将变成抽象类。
abstract class Shape {
init {
println("执行 Shape 的初始化块。。。 ")
}
var color = ""
//定义一个计算周长的抽象方法
abstract fun calPerimeter(): Double
//定义一个代表形状的抽象的只读属性
//抽象周性不需要初始值
abstract val type: String
//定义 Shape 的构造器,该构造器并不是用于创建 Shape 对象的,而是用于被子类调用
constructor() {
}
constructor(color: String) {
println("执行 Shape 的构造器...")
this.color = color
}
}
class Triangle(color: String, var a: Double, var b: Double, var c: Double) : Shape(color) {
fun setSides(a: Double, b: Double, c: Double) {
if (a >= b + c || b >= a + c || c >= a + b) {
println("三角形两边之和必须大于第三边")
return
}
this.a = a
this.b = b
this.c = c
}
//重写 Shape 类的计算周长的抽象方法
override fun calPerimeter(): Double {
return a + b + c
}
//重写 Shape 类的代表形状的抽象属性
override val type: String
get() = "三角形"
}
class Circle(color: String, var radius: Double) : Shape(color) {
override val type: String
get() = "圆形"
//重写 Shape 类的计算周长的抽象方法
override fun calPerimeter(): Double {
return Math.PI * 2 * radius
}
}
fun main(args: Array) {
var s1: Shape = Triangle("黑色", 3.0, 4.0, 5.0)
var s2: Shape = Circle("黄色", 6.0)
println(s1.type)
println(s1.calPerimeter())
println(s2.type)
println(s2.calPerimeter())
}
abstract 不能用于修饰局部变量, Kotlin 没有抽象变量的说法; abstract也不能用于修饰构造器,没有抽象构造器,抽象类中定义的构造器只能是普通构造器。
使用 abstract关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法体,因此 abstract 方法不能定义为 private 访问权限,即 private 和 abstract不能同时修饰方法。
与 Java 类似的是, Kotlin 也允许使用抽象成员重写非抽象成员。例如如下代码:
open class Base1 {
open fun foo() {}
}
abstract class Foo:Base1(){
override abstract fun foo()
}
抽象类的作用
从前面的示例程序可以看出,抽象类不能创建实例,只能当成父类来被继承。从语义的角度来看,抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象。从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为其子类的模板,从而避免了子类设计的随意性。抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会大致保留抽象类的行为方式。
如果编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给其子类实现,这就是一种模板模式,模板模式也是十分常见且简单的设计模式之一。例如前面介绍的 Shape、 Circle 和 Triangle 三个类 ,已经使用了模板模式 。下面再介绍一个模板模式的范例,在这个范例的抽象父类中,父类的普通方法依赖于一个抽象方法,而抽象方法则推迟到子类中提供实现。
//定义带转速属性的主构造器
abstract class SpeedMeter(var turnRate: Double) {
//把返问车轮半径的方法定义成抽象方法
abstract fun calGirth(): Double
//定义计算速度的通用算法
fun getSpeed(): Double {
//速度等于车轮周长*转速
return calGirth() * turnRate
}
}
class CarSpeedMeter(var radius: Double) : SpeedMeter(0.0) {
override fun calGirth(): Double {
return radius * 2 * Math.PI
}
}
fun main(args: Array) {
val csm = CarSpeedMeter(0.28)
csm.turnRate = 15.0
println(csm.getSpeed())
}
SpeedMeter 类中提供了车速表的通用算法,但一些具体的实现细节则推迟到其子类CarSpeedMeter 中实现。这也是一种典型的模板模式。模板模式在面向对象的软件中很常用,其原理简单,实现也很简单。下面是使用模板模式的一些简单规则。
- 抽象父类可以只定义需要使用的某些方法,把不能实现的部分抽象成抽象方法,留给其子类去实现。
- 父类中可能包含需要调用其他系列方法的方法,这些被调方法既可以由父类实现,也可以由其子类实现。父类中提供的方法只是定义了一个通用算法,其实现也许并不完全由自身来完成,而必须依赖于其子类的辅助。
密封类
密封类这个名字很奇怪,光看名字很容易把它当成被“密封”的类,以为它不能派生子类。但实际上,密封类是一种特殊的抽象类,专门用于派生子类。
密封类与普通抽象类的区别在于: 密封类的子类是固定的。密封类的子类必须与密封类本身在同一个文件中,在其他文件中则不能为密封类派生子类,这样就限制了在其他文件中派生子类。
在Kotlin1. 1之前,密封类的子类必须在密封类的内部声明。
如下程序定义了 一个密封类和两个子类 :
//定义一个密封类,其实就是抽象类
sealed class Apple {
abstract fun taste()
}
open class RedFuji : Apple() {
override fun taste() {
println("红富士苹果香甜可口")
}
}
data class Gala(var weight: Double) : Apple() {
override fun taste() {
println("嘎拉果更消脆,重量为:${weight}")
}
}
fun main(args: Array) {
//使用 Apple 声明变盘,用子类实例赋值
var ap1:Apple= RedFuji()
var ap2 :Apple = Gala(4.5)
ap1.taste()
ap2.taste()
}
上面程序中定义了一个密封类,接下来即可在该密封类中定义抽象方法,由此可见,密封类的本质就是抽象类。
密封类经过 Kotlin 编译器编译之后就得到一个抽象类的 class 文件,只不过该抽象类的普通构造器都会声明为 private 权限,而 Kotlin 会为之创建一个对应的带kotlin.jvm.intemal.DefaultConstructorMarker参数的构造器。
定义密封类之后,就像定义了抽象类,接下来即可在同一个文件中为该密封类派生子类,就像为普通抽象类派生子类一样,如上面程序代码所示。根据密封类的规则我们知道,密封类的所有构造器都必须是 private 的,不管开发者是否使用 private修饰,系统都会为之自动添加private修饰。
最后有一点需要说明的是,密封类的直接子类必须与密封类位于同一个文件中,但密封类的间接子类(子类的子类)则无须在同一个文件中。
使用密封类的好处是 : 它的子类是固定的,编译器可以清楚地知道密封类只可能有固定数量的子类。因此使用 when表达式判定密封类时,编译器可以清楚地知道是否覆盖了所有情况,从而判断是否需要添加 else 子句。
fun judge(app: Apple) {
when (app) {
is RedFuji -> {
println("红富士苹果 ")
}
is Gala -> {
println("嘎拉果")
}
}
}
接口
Kotlin的接口是以 Java 8接口为蓝本设计的 , 因此 Kotlin 的接口与Java 8 的接口非常相似。
接口的定义
和类定义不同,定义接口时不再使用class关键字,而是使用 interface 关键字 。接口定义的基本语法如下
[修饰符) interface 接口名:父接口 1, 父接口 2... {
零个到多个属性定义.. .
零个到多个方法定义 ...
零个到多个嵌套类、嵌套接口 、 嵌套枚举定义 .. .
}
对上面语法的详细说明如下:
- 修饰符可以是 public | internal | private 中的任意一个,或完全省略修饰符。如果省略了访问控制符,则默认采用 public。
- 接口名应与类名采用相同的命名规则。
- 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
与Java8相似的是,Kotlin的接口既可包含抽象方法,也可包含非抽象方法,这是非常自由的。但接口中的属性没有幕后字段,因此无法保存状态,所以接口中的属性要么声明为抽象属性,要么为之提供 setter、 getter方法。
由于接口定义的是一种规范,因此接口中不能包含构造器和初始化块定义。对比接口和类的定义方式,不难发现接口中的成员比类中的成员少了两种。
接口中定义的方法既可是抽象方法,也可是非抽象方法。如果一个方法没有方法体, Kotlin会自动为该方法添加 abstract 修饰符;同理,如果一个只读属性没有定义getter方法, Kotlin会自动为该属性添加 abstract 修饰符;如果一个读写属性没有定义 getter、 setter 方法, Kotlin会自动为该属性添加 abstract修饰符。
Kotlin接口与 Java接口还有一点区别:Java接口中的所有成员都会自动使用 public修饰,如果为这些成员指定访问权限,也只能指定 public 访问权限;但 Kotlin 接口中的成员可支持 private 和 public 两种访问权限 。具体规则如下:
- 对于需要被实现类重写的成员,如抽象方法、抽象属性,只能使用 public 修饰。如果要添加访问控制符,则只能用 public;如果不加访问权限,则系统默认添加 public。
- 对于不需要被实现类重写的成员,如非抽象方法、非抽象属性、嵌套类(包括嵌套抽象类)、嵌套接口、嵌套枚举,都可使用 private 或 public 修饰,我们可以根据需要添加 private 修饰符将这些成员限制在接口内访问:如果不加访问权限,则系统默认添加 public。
下面定义一个接口:
interface Outputable {
//只读属性定义了 getter 方法,非抽象属性
val name: String
get() = "”输出设备"
//只读属性没有定义 getter 方法,抽象属性
val brand: String
//读写属性没有定义 getter、setter方法,抽象属性
var category: String
//接口中定义的抽象方法
fun out()
fun getData(msg: String)
//在接口中定义的非抽象方法,可使用 private 修饰
fun print(vararg msgs: String) {
for (msg in msgs) {
println(msg)
}
}
//在接口中定义的非抽象方法,可使用 private 修饰
fun test(){
println("”接口中test()方法")
}
}
Kotlin 允许在接口中定义普通方法,上面的 Outputable 接口中定义了print()和test()两个非抽象方法,它们都有方法体,因此系统不会自动为它们添加 abstract修饰符。由于这两个方法是非抽象方法,因此我们可以为它们添加 private 修饰符,如果不添加任何修饰符,则系统默认为它们添加 public 修饰符 。
上面接口中还定义了三个属性,其中 name 是一个只读属性,程序为该属性定义了getter方法,因此该属性不是抽象属性,系统无须为之添加 abstract 修饰符: brand 是只读属性,没有getter 方法,它是抽象属性,系统会自动为之添加 abstract 修饰符; category 是读写属性,程序没有为之定义 getter 和 setter 方法,它也是抽象属性,系统会自动为之添加 abstract 修饰符。
接口的继承
接口的继承和类继承不一样,接口完全支持多继承,即一个接口可以有多个直接父接口。和类继承相似,子接口继承某个父接口,将会获得父接口中定义的所有方法、属性 。
一个接口继承多个父接口时,多个父接口排在英文冒号(:)之后,它们之间以英文逗号隔开。
interface InterfaceA {
val propA: Int
get() = 5
fun testA()
}
interface InterfaceB {
val popB: Int
get() = 6
fun testB()
}
interface InterfaceC : InterfaceA, InterfaceB {
val popC: Int
get() = 7
fun testC()
}
使用接口
接口不能用于创建实例,但可以用于声明变量。当使用接口来声明变量时,这个引用类型的变量必须引用到其实现类的对象。除此之外,接口的主要用途就是被实现类实现 。归纳起来,接口主要有如下用途:
- 定义变量,也可用于进行强制类型转换。
- 被其他类实现。
一个类可以实现一个或多个接口,直接将被实现的多个接口、父类放在英文冒号之后,且父类、接口之间没有顺序要求,只要将它们用英文逗号隔开即可。
[修饰符] class类名: 父类, 接口1,接口2...{
类体部分
}
实现接口与继承父类相似,一样可以获得所实现接口中定义的属性(包括抽象属性和非抽象属性)、方法(包括抽象方法和非抽象方法)。
一个类实现了一个或多个接口之后,这个类必须完全实现这些接口中所定义的全部抽象成员(也就是重写这些抽象方法和抽象属性);否则,该类将保留从父接口那里继承到的抽象成员,该类也必须定义成抽象类。
一个类实现某个接口时,该类将会获得接口中定义的属性、方法等 ,因此可以把实现接口理解为一种特殊的继承,相当于实现类继承了一个更抽象的类。
//定义一个Product接口
interface Product {
fun getProduceTime(): Int
}
//定义一个Outputable接口
interface Outputable {
//只读属性定义了 getter 方法,非抽象属性
val name: String
get() = "”输出设备"
//只读属性没有定义 getter 方法,抽象属性
val brand: String
//读写属性没有定义 getter、setter方法,抽象属性
var category: String
//接口中定义的抽象方法
fun out()
fun getData(msg: String)
//在接口中定义的非抽象方法,可使用 private 修饰
fun print(vararg msgs: String) {
for (msg in msgs) {
println(msg)
}
}
//在接口中定义的非抽象方法,可使用 private 修饰
fun test() {
println("”接口中test()方法")
}
}
const val MAX_CACHE_LINE = 10
//让 Printer 类实现 Outputable 和 Product 接口
class Printer : Product, Outputable {
private val printData = Array(MAX_CACHE_LINE, { "" })
//用以记录当前需要打印的作业数
private var dataNum = 0
//重写接口的抽象只读属性
override val brand: String
get() = "HP"
////重写接口的抽象读写属性
override var category: String = "输出外设"
override fun getProduceTime(): Int {
return 45
}
override fun out() {
//只要还有作业,就继续打印
while (dataNum > 0) {
println("打印机打印:${printData[0]}")
//把作业队列整体前移一位,并将剩下的作业数减1
System.arraycopy(printData, 1, printData, 0, --dataNum)
}
}
override fun getData(msg: String) {
if (dataNum >= MAX_CACHE_LINE) {
println("输出队列已满,添加失败")
} else {
//把打印数据添加到队列里, 已保存数据的数量+1
printData[dataNum++] = msg
}
}
}
fun main(args: Array) {
//创建一个 Printer 对象,当成 Output 使用
var o: Outputable = Printer()
o.getData("java")
o.getData("kotlin")
o.out()
//调用 Outputable 接口中定义的非抽象方法
o.print("xq", "sq", "xy")
o.test()
//创建一个 Printer 对象,当成 Product 使用
val p: Product = Printer()
println(p.getProduceTime())
//所有接口类型的引用变盘都可直接赋给 Any 类型的变量
val obj: Any = p
}
从上面程序中可以看出, Printer类实现了 Outputable和 Product接口,因此 Printer对象既可直接赋值给 Outputable变量,也可直接赋值给 Product变量。就好像 Printer类既是 Outputable 类的子类,也是 Product类的子类,这就是 Kotlin 提供的模拟多继承。
上面程序中 Printer实现了 Outputable接口,即可获取 Outputable接口中定义的 print()和 test()两个非抽象方法,因此 Printer 实例可以直接调用这两个默认方法。
在实现接口中的成员时,必须使用 public访问控制符,因为接口中的成员都是 public 的,而子类(相当于实现类)重写父类方法时访问权限只能更大或者相等,所以实现类实现接口中的成员时只能使用 public访问权限。
接口不能显式继承任何类,但所有接口类型的变量都可以直接赋给 Any 类型的变量。所以在上面程序中可以把 Product 类型的变量直接赋给 Any 类型的变量,这是利用向上转型来实现的。因为编译器知道任何 Kotlin对象都必须是 Any 或其子类的实例,
接口和抽象类
接口和抽象类有一些相似之处,它们都具有如下特征。
- 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
- 接口和抽象类都可以包含抽象成员,实现接口或继承抽象类的普通子类都必须实现这些抽象成员。
但接口和抽象类之间的差别也很大,这种差别主要体现在二者的设计目的上。下面具体分析二者的差别。
接口作为系统与外界交互的窗口,体现的是一种规范。 对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法、属性的形式来提供);对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务(就是如何调用方法、访问属性) 。 当在一个程序中使用接口时,接口是多个模块之间的耦合标准:当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。
从某种程度上看,接口类似于整个系统的“总纲”,它制定了系统各模块应该遵循的标准,因此一个系统中的接口不应该经常改变。一旦接口被改变,对整个系统甚至其他系统的影响将是辐射式的,导致系统中大部分类都需要改写 。 抽象类则不一样,抽象类作为系统中多个子类的共同父类,所体现的是一种模板模式设计。
抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能(那些已经提供实现的方法),但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同方式。
此外,接口和抽象类在用法上也存在如下差别:
- 接口中不包含构造器;但抽象类中可以包含构造器,抽象类中的构造器并不是用于创建对象的,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
- 接口中不能包含初始化块;但抽象类中则完全可以包含初始化块。
- 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补 Kotlin单继承的不足。