Kotlin(六)多态和扩展
6.1 多态的不同方式
当我们用一个类继承父类时,这就是子类型多态
。另外一种是参数多态
,泛型就是其中的一种表现。还有C++中的运算符重载属于特设多态
。Kotlin中除了运算符重载,还有扩展等表现多态的形式
6.1.1 子类型多态
看一个代码的定义
class CustomerDatabaseHelper(context: Context)
: SQLiteOpenHelper(context, "kotlinDemo.db",
cursorFactory, version
) {
override fun onCreate(db: SQLiteDatabase?) {
val sql = "create table if not exists StableName(id integer PRIMARY KEY autoincrement,uniqueKey varchar(32))"
db?.execSQL(sql)
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
}
}
使用父类DatabaseHelper的所有方法,这种用子类型替换超类型实例的行为,就是我们通常说的子类型多态。
6.1.2 参数多态
可能会这样写一个方法
fun persist(customer:Customer){
db.save(customer.uniqueKey,customer)
}
需求会不断变动,每个类型都想Customer一样重写一份persist 不适合。采取的是键值对,所以获取不同类型对应的uniqueKey
interface KeyI{
val uniqueKey : String
}
class ClassA(override val uniqueKey:String):KeyI{
}
class ClassB(override val uniqueKey:String):KeyI{
}
A,B都具备uniqueKey,改写persist 方法,最常见的泛型
fun <T:KeyI> persist(t:T){
db.save(t.uniqueKey,t)
}
6.1.3 对第三方进行扩展
假设对应的业务类ClassA,ClassB是第三方引入的,且不可以被修改。如果想扩展一些方法,如转化成json。利用之前的多态比较麻烦。可以利用扩展,可以换一种思路解决问题:
fun ClassA.toJson():String = {
...
}
java中我们可以用设计及模式解决类似扩展问题,kotlin的扩展,避免修改父类,避免了因为父类参数修改导致子类出错的问题。扩展也被叫做特设多态的一种技术
6.1.4 特设多态---运算符重载
当拟定一一个sum的时候,可能想这么写
-> 错误的例子
fun <T> sum(x:T,y:T) : T = x + y
但是不是说有类型支持 + 法则,所以编译器报错。
如果换一种方式,设计Summable接口,然后让支持的加法操作实现plusThat
interface Summable<T>{
fun plusThat(that:T):T
}
data class Len(val v:Int):Summable<Len>{
override fun plusThat(that: Len)= Len(this.v+that.v)
}
如果针对不可以修改的第三方类扩展加法,通过子类型进行多态的思路手段也会遇到问题
所有用我们的”特设多态“运算符重载
data class Area(val value: Double)
operator fun Area.plus(that: Area): Area {
return Area(this.value + that.value)
}
fun main() {
println(Area(1.0) + Area(2.0))
}
kotlin通过operator 标记重载一个操作符或者实现一个约定。
plus是Kotlin内置的可重载运算符
6.2 为别的类添加方法和属性
6.2.1 扩展与开放封闭原则
修改源码,我们应该遵循开放封闭原则,即软件是可以扩展的不是修改的
开放封闭原则
是所有面向对象原则的核心。软件本身所追求的目标就是封装变化,降低耦合,开闭原则是这个目标的直接体现。其他的设计原则有时候是为了这一目标服务,如,替换原则,实现最佳的,正确的继承层次,不会违反开放封闭原则。
实际上不容乐观,引入了第三方库,某天需求发生变动,当前库无法满足,库的作者暂时没更新计划,你可能会尝试修改库源码,这就违背了开放封闭原则。随着需求不断变更,就会把问题滚雪球一样的放大。
java中的惯常应对方案是让第三方库继承一个子类,然后添加新功能。然而强行继承可能会违背"里氏替换原则"
Kotlin提供的扩展,大部分情况都是一种更好的选择,从而我们可以合理的遵循软件设计原则。
6.2.2 使用扩展函数、属性
扩展函数的声明非常简单,他的关键<Type>. 我们需要一个接收者(receivier type 通常是类或者接口名)类型作为他的前缀
MutableList<Int> 为例子,扩展一个方法exchange:
fun MutableList<Int>.exchange(fromIndex:Int,toIndex:Int){
val tmp = this[fromIndex]
this[fromIndex] = this[toIndex]
this[toIndex] = tmp;
}
fun main() {
val list = mutableListOf(1,2,3)
list.exchange(0,2)
println(list)
}
MutableList<T> 是Kotlin 的Collections标准库中的List容器,作为receievier type,exchange是扩展函数名,其他与普通函数没有区别,这里的this代表的是接受者类型的对象,kotlin严格控制接收者可空类型,如果你的函数是可空的,你需要重写可空类型的扩展函数。
- 扩展函数实现机制----对性能影像
@Metadata(
mv = {1, 1, 16},
bv = {1, 0, 3},
k = 2,
xi = 2,
d1 = {"\u0000\u0014\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\u001a\u0006\u0010\u0000\u001a\u00020\u0001\u001a \u0010\u0002\u001a\u00020\u0001*\b\u0012\u0004\u0012\u00020\u00040\u00032\u0006\u0010\u0005\u001a\u00020\u00042\u0006\u0010\u0006\u001a\u00020\u0004¨\u0006\u0007"},
d2 = {"main", "", "exchange", "", "", "fromIndex", "toIndex", "use_kt_poject.main"}
)
public final class KT扩展Kt {
public static final void exchange(@NotNull List $this$exchange, int fromIndex, int toIndex) {
Intrinsics.checkNotNullParameter($this$exchange, "$this$exchange");
int tmp = ((Number)$this$exchange.get(fromIndex)).intValue();
$this$exchange.set(fromIndex, $this$exchange.get(toIndex));
$this$exchange.set(toIndex, tmp);
}
public static final void main() {
List list = CollectionsKt.mutableListOf(new Integer[]{1, 2, 3});
exchange(list, 0, 2);
boolean var1 = false;
System.out.println(list);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
反编译后可以看到exchange近似理解为静态方法。
Java中静态方法特点:
- 独立于该类的任何对象,且不依赖特定实例,被该类所有实例共享
- public 修饰的static方法是全局的
结论:扩展函数不会带来额外的性能销耗。
- 扩展函数的作用域
一个包内可以直接调用exchange,其他包中需要import相应的方法。实际开发我们会将扩展函数定义在一个Class内部统一管理。
class Extends{
fun MutableList<Int>.exchange(fromIndex:Int,toIndex:Int){
val tmp = this[fromIndex]
this[fromIndex] = this[toIndex]
this[toIndex] = tmp;
}
}
反编译后你会发现
public final class Extends {
public final void exchange(@NotNull List $this$exchange, int fromIndex, int toIndex) {
Intrinsics.checkNotNullParameter($this$exchange, "$this$exchange");
int tmp = ((Number)$this$exchange.get(fromIndex)).intValue();
$this$exchange.set(fromIndex, $this$exchange.get(toIndex));
$this$exchange.set(toIndex, tmp);
}
没有了static关键字,所以扩展方法在一个类class内部时,我们只能在该类和该类的子类中进行调用。
- 扩展属性
给MutableList<Int> 添加一个判断是否是偶数判断的属性sumIsEven:
val kotlin.collections.MutableList<kotlin.Int>.sumIsEven: Boolean
get() = this.sum() % 2 == 0
val newlist = mutableListOf(2, 2, 4)
println(newlist.sumIsEven)
如果加上默认值会报错
会报错
val kotlin.collections.MutableList<kotlin.Int>.sumIsEven: Boolean = false
get() = this.sum() % 2 == 0
因为扩展属性没有实际将成员插入类中,因此对扩展属性幕后字段无效,这就是为什么我们默认值失败原因。他的行为只能显示用getter和setter定义。
幕后字段
kotlin中,如果属性存在访问器使用默认实现,那么Kotlin会自动提供幕后字段filed,仅用于自定义getter和setter
6.2.3 扩展的特殊情况
- 类似java的扩展函数
如果要声明静态的扩展函数,那么必须定义在半生对象。
class Son {
companion object {
val age = 10
}
}
fun Son.Companion.foo() {
println("age=$age")
}
object Test{
@JvmStatic
fun main() {
Son.foo()
}
}
我们就能在Son没实例对象的情况下,也能调用到这个扩展函数。但是想让第三方库也可以这样,并不是所有第三方库都有半生对象,我们只能通过他的实例调用,但这会带来很多不必要麻烦。
- 同名的类成员方法优先级高于扩展函数
class Son2{
fun foo() = println("son memeber foo")
}
fun Son2.foo() = println("son extends foo")
fun main() {
Son2().foo()
}
> >>
son memeber foo
- 类的实例与接收者的实例。
我们说过,扩展函数调用this,指代的是接收者类型的实例。
扩展函数声明在object内部,我们用this@类名,强行指定调用的this.
class Son2{
fun foo() = println("son in class Son")
}
object Parent{
fun foo() = println("son in Parent Son")
@JvmStatic
fun main(args: Array<String>) {
fun Son2.foo2(){
this.foo()
this@Parent.foo()
}
Son2().foo2()
}
}
>>>>>
son in class Son
son in Parent Son
6.2.4 标准库的扩展函数:run,let,also,takeif
- run
fun testFoo(){
val nickName = "Prefert"
run {
val nickName = "David"
println(nickName)//David
}
println(nickName)//Prefert
}
run 函数我们有一个单独的作用域,能重新定义nickName,并且作用域只在run中
看下源代码
public inline fun <T, R> T.run(block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
run是任何类型T的扩展函数,run中执行了返回类型R的扩展函数block,组中返回了扩展函数的结果
场景:用户点击新人领取奖励按钮,如果用户没有登录则弹出loginDialog,已经登录弹出领取奖励getNewAccountDialog,可以用如下代码
run{
if(!isLogin) loginDialog else getNewAccountDialog
}.show()
- let
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
前面的文章我们提到过apply,let和apply类似,唯一不同是返回值:apply返回的是原来的对象,let返回的是闭包里面的值
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}
举个栗子:
data class StudentA(val age: Int)
class Kot{
val student:StudentA? = getStu()
fun dealStu(){
val result = student?.let{
println(it.age)
it.age
}
}
}
fun getStu():StudentA{
return StudentA(11)
}
let返回的是闭包的最后一行,当Student部位null的时候,才会打印并放回他的年龄,和run一样,他同样限制了作用域
- also ,可以看做let和apply的加强版
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}
与apply一致,他的返回值是改函数的接收者
class Kot{
var age = 0
val student:StudentA? = getStu()
fun dealStu(){
val result = student?.also{stu->
this.age += stu.age
println(this.age)
println(stu.age)
stu.age
}
}
}
also 返回student,并且年龄age增加
如果换成also---》apply 我们将无法访问Kot 下的age
4.takeIf
public inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
contract {
callsInPlace(predicate, InvocationKind.EXACTLY_ONCE)
}
return if (predicate(this)) this else null
}
如果我们不仅仅想判断空,还要加入条件,那么用takeIf
val result = student.takeIf { it.age >=18 }.let {
....
}