多态的不同方式 当一个子类继承父类的时候,这就是子类型多态 ;另一种熟悉的多态是参数多态 ,泛型就是参数多态常见的形式。
对第三方类扩展 考虑业务类 ClassA、ClassB是第三方引入的并且不能修改 ,如果我们想要给它们扩展一些方法,比如将对象转换为 Json 字符串,那么利用以前多态技术就会显得麻烦。幸运的是,Kotlin 支持扩展语法:
1 2 3 fun ClassA.toJson () : String = { ... }
就可以很方便地添加了toJson方法,需要注意的是,扩展属性和方法的实现运行在ClassA的实例 ,他们的定义操作不会修改 ClassA 类本身,所以被扩展的第三方类免于被污染。
特设多态与运算符重载 除了子类型多态、参数多态外,还有一种灵活的多态——特设多态 。可能概念不好理解,举个具体的例子,你想定义一个通用的sum方法,也许会这么写:
1 fun <T> sum (x: T , y: T ) : T = x + y
但编译器会报错,因为某些类型不一定支持加法操作。这时候,我们希望定义一种通用的“加法语义上的操作”,可以定义一个通用的 Summable 接口,然后让需要支持假发操作的类来实现它:
1 2 3 4 5 6 7 interface Sumable <T > { fun plusThat (that: T ) : T } data class Len (val v: Int ): Sumable<Len> { override fun plusThat (that: Len ) = Len(this .v + that.v) }
可以发现,这并没有什么问题。然而,如果要针对不可修改的第三方类扩展加法操作时,这种方式也会遇到问题。于是,又想到了Kotlin的扩展,针对以上例子,我们完全可以采用扩展的语法来解决问题,此外,Kotlin 原生支持运算符重载 可以很好解决上述问题:
1 2 3 4 5 6 7 8 9 data class Area (val value: Double )operator fun Area.plus (that: Area ) : Area { return Area(this .value + that.value) } fun main (args: Array <String >) { println(Area(1.0 ) + Area(2.0 )) }
通过 operator 关键字以及Kotlin中内置可重载的运算符plus ,就实现了功能。operator 的作用是,将一个函数标记为重载一个操作符或者实现一个约定,这里的plus是Kotlin规定的函数名 。除了plus,我们还可以通过重载减法(minus)、乘法(times)、触发(div)、取余(mod,在kotlin 1.1 版本开始被 rem 替代)。此外,回忆kotlin中常用语法,也是用这种神奇的语言特性实现的,比如:
扩展: 为别的类添加方法、属性 继续深入Kotlin 的特设多态语言特性
扩展与开放封闭原则 熟悉设计模式的读者知道,在修改现有代码时,我们应该遵循开放封闭原则,对扩展开放,对修改封闭。然而实际并不乐观,比如Android开发,为实现某个需求,引入了第三方库,但是需求发生变动后,当前库无法满足需求,且库的作者没有升级计划。这时候你也许就会考虑对源码修改,这就违背了开放封闭原则。
Java中一种惯常做法是继承第三方类,添加新功能,但是,强行的继承可能违背“里氏替换原则”。更合理的方案,就是通过Kotlin的扩展功能。
使用扩展函数、属性 扩展函数的声明的关键字是 。此外,我们需要一个“接收者类型”(通常是类或者接口)来作为它的前缀,以为 MutableList扩展exchange方法为例:
1 2 3 4 5 fun MutableList<Int> .exchange (fromIndex: Int , toIndex: Int ) { val temp = this [fromIndex] this [fromIndex] = this [toIndex] this [toIndex] = temp }
MutableList 是Kotlin标准库中的类,这里作为接收者类型,exchange是扩展函数名。Kotlin的this要比Java的更灵活,这里扩展函数体内的this代表的是接收者类型的对象 。注意,Kotlin中是严格区分接收者是否可空的,如果你的函数是可空的,你需要重写一个可空类型的扩展函数 。
扩展函数的实现机制 扩展函数这么方便,会不会对性能造成影响呢?以 MutableList.exchange 为例,对应的Java代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Metadata( mv = {1, 5, 1}, k = 2, d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\u001a \u0010\u0000\u001a\u00020\u0001*\b\u0012\u0004\u0012\u00020\u00030\u00022\u0006\u0010\u0004\u001a\u00020\u00032\u0006\u0010\u0005\u001a\u00020\u0003¨\u0006\u0006"}, d2 = {"exchange", "", "", "", "fromIndex", "toIndex", "CommoKotlin"} ) public final class KotClassKt { public static final void exchange (@NotNull List $this $exchange, int fromIndex, int toIndex) { Intrinsics.checkNotNullParameter($this $exchange, "$this$exchange" ); int temp = ((Number)$this $exchange.get(fromIndex)).intValue(); $this $exchange.set(fromIndex, $this $exchange.get(toIndex)); $this $exchange.set(toIndex, temp); } }
结合上述Java代码可以看出,我们可以将扩展函数近似理解为静态方法 。我们知道静态方法的特点:不依赖类的特定实例,被该类所有的实例共享,并且,用public修饰,本质上也就是个全局方法,所以,扩展函数不会带来额外的性能消耗 。
扩展函数的作用域 一般来说,我们习惯将扩展函数直接定义在包内,例如之前的 exchange 例子,我们可以将其放在 com.example.extension包下:
1 2 3 4 5 package com.example.extensionfun MutableList<Int> .exchange (fromIndex: Int , toIndex: Int ) { ... }
我们知道,在同一个包内是可以直接调用exchange方法的,如果需要在其他包中调用,只需要import即可 ,这与Java全局静态方法类似。与此同时,在实际开发中,我们可能会将扩展函数定义在一个 Class 内部统一管理:
1 2 3 4 5 6 7 class Extends { fun MutableList<Int> .exchange (fromIndex: Int , toIndex: Int ) { val temp = this [fromIndex] this [fromIndex] = this [toIndex] this [toIndex] = temp } }
但你会发现,之前的exchange方法无法调用了!我们看下它的Java源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Metadata( mv = {1, 5, 1}, k = 1, d1 = {"\u0000\u001c\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J \u0010\u0003\u001a\u00020\u0004*\b\u0012\u0004\u0012\u00020\u00060\u00052\u0006\u0010\u0007\u001a\u00020\u00062\u0006\u0010\b\u001a\u00020\u0006¨\u0006\t"}, d2 = {"Lcom/glassx/Extends;", "", "()V", "exchange", "", "", "", "fromIndex", "toIndex", "CommoKotlin"} ) public final class Extends { public final void exchange (@NotNull List $this $exchange, int fromIndex, int toIndex) { Intrinsics.checkNotNullParameter($this $exchange, "$this$exchange" ); int temp = ((Number)$this $exchange.get(fromIndex)).intValue(); $this $exchange.set(fromIndex, $this $exchange.get(toIndex)); $this $exchange.set(toIndex, temp); } }
我们才发现exchange方法没有static关键字了,所以,当扩展方法在一个 Class 内部时,我们只能在该类和该类的子类中调用 。
扩展属性 与扩展函数一样,我们还能为一个类添加扩展属性,比如为MutableList添加一个判断和是否为偶数的属性:
1 2 3 4 5 6 val MutableList<Int >.sumIsEven: Boolean get () = this .sum() % 2 == 0 val list = mutableListOf(2 ,2 ,4 )list.sumIsEven
但是,如果你准备给这个属性添加默认值,并且写出如下代码:
1 2 val MutableList<Int >.sumIsEven: Boolean = false get () = this .sum() % 2 == 0
代码会编译不通过,告知扩展属性不能有初始化器。这是为什么呢?其实,与扩展函数一样,其本质也是对应Java中的静态方法,反编译成Java后,会看到一个 getSumIsEven 的静态方法:
1 2 3 4 5 6 7 8 9 10 11 12 @Metadata( mv = {1, 5, 1}, k = 2, d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u000b\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\"\u001b\u0010\u0000\u001a\u00020\u0001*\b\u0012\u0004\u0012\u00020\u00030\u00028F¢\u0006\u0006\u001a\u0004\b\u0004\u0010\u0005¨\u0006\u0006"}, d2 = {"sumIsEven", "", "", "", "getSumIsEven", "(Ljava/util/List;)Z", "CommoKotlin"} ) public final class ExtendsKt { public static final boolean getSumIsEven (@NotNull List $this $sumIsEven) { Intrinsics.checkNotNullParameter($this $sumIsEven, "$this$sumIsEven" ); return CollectionsKt.sumOfInt((Iterable)$this $sumIsEven) % 2 == 0 ; } }
由于扩展没有实际地将成员插入类中,因此对扩展属性来说幕后字段是无效的,它们的行为只能由显式提供的 getters 和setters 定义 。
幕后字段:如果属性中存在访问器使用默认实现,那么Kotlin 会自动提供幕后字段 field ,其仅可用于 getter 和 setter 中。
扩展的特殊情况 类似Java的静态扩展函数 在Kotlin中,如果要声明一个静态的扩展函数,必须要有 伴生对象(companion object)上,所以我们要这样定义带有伴生对象的类:
1 2 3 4 5 class Son { companion object { val age = 10 } }
在已有伴生对象的情况下,如果不想再Son中定义扩展函数,而是在Son的伴生对象上定义,可以这么写:
1 2 3 4 5 6 7 8 fun Son.Companion.foo () { println("age = $age " ) } fun main (args: Array <String >) { Son.foo() }
成员方法优先级总是高于扩展函数 如果扩展函数和现有类的成员方法一样,那么优先调用成员方法 ,这一点好理解,我们不应该更改原有实现。
类的实例与接收者实例 略,没看清楚表达什么
标准库中的扩展函数:run、let、also、takeIf 略
Android中扩展的应用 略
扩展不是万能的 略